Skip to main content

DatabaseAdapter

Trait DatabaseAdapter 

Source
pub trait DatabaseAdapter:
    Send
    + Sync
    + 'static {
Show 31 methods // Required methods fn execute_where_query<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, view: &'life1 str, where_clause: Option<&'life2 WhereClause>, limit: Option<u32>, offset: Option<u32>, order_by: Option<&'life3 [OrderByClause]>, ) -> Pin<Box<dyn Future<Output = Result<Vec<JsonbValue>, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait; fn execute_with_projection<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>( &'life0 self, view: &'life1 str, projection: Option<&'life2 SqlProjectionHint>, where_clause: Option<&'life3 WhereClause>, limit: Option<u32>, offset: Option<u32>, order_by: Option<&'life4 [OrderByClause]>, ) -> Pin<Box<dyn Future<Output = Result<Vec<JsonbValue>, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, Self: 'async_trait; fn database_type(&self) -> DatabaseType; fn health_check<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<(), FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, Self: 'async_trait; fn pool_metrics(&self) -> PoolMetrics; fn execute_raw_query<'life0, 'life1, 'async_trait>( &'life0 self, sql: &'life1 str, ) -> Pin<Box<dyn Future<Output = Result<Vec<HashMap<String, Value>>, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait; fn execute_parameterized_aggregate<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, sql: &'life1 str, params: &'life2 [Value], ) -> Pin<Box<dyn Future<Output = Result<Vec<HashMap<String, Value>>, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait; // Provided methods fn execute_where_query_arc<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, view: &'life1 str, where_clause: Option<&'life2 WhereClause>, limit: Option<u32>, offset: Option<u32>, order_by: Option<&'life3 [OrderByClause]>, ) -> Pin<Box<dyn Future<Output = Result<Arc<Vec<JsonbValue>>, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait { ... } fn execute_with_projection_arc<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, request: &'life1 ProjectionRequest<'life2>, ) -> Pin<Box<dyn Future<Output = Result<Arc<Vec<JsonbValue>>, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait { ... } fn execute_row_query<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>( &'life0 self, view_name: &'life1 str, columns: &'life2 [ColumnSpec], where_sql: Option<&'life3 str>, order_by: Option<&'life4 str>, limit: Option<u32>, offset: Option<u32>, ) -> Pin<Box<dyn Future<Output = Result<Vec<Vec<ColumnValue>>, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, Self: 'async_trait { ... } fn execute_row_query_with_session<'life0, 'life1, 'life2, 'life3, 'life4, 'life5, 'life6, 'life7, 'async_trait>( &'life0 self, view_name: &'life1 str, columns: &'life2 [ColumnSpec], where_sql: Option<&'life3 str>, order_by: Option<&'life4 str>, limit: Option<u32>, offset: Option<u32>, _session_vars: &'life5 [(&'life6 str, &'life7 str)], ) -> Pin<Box<dyn Future<Output = Result<Vec<Vec<ColumnValue>>, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, 'life5: 'async_trait, 'life6: 'async_trait, 'life7: 'async_trait, Self: 'async_trait { ... } fn execute_parameterized_aggregate_with_session<'life0, 'life1, 'life2, 'life3, 'life4, 'life5, 'async_trait>( &'life0 self, sql: &'life1 str, params: &'life2 [Value], _session_vars: &'life3 [(&'life4 str, &'life5 str)], _routing: ReadRouting, ) -> Pin<Box<dyn Future<Output = Result<Vec<HashMap<String, Value>>, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, 'life5: 'async_trait, Self: 'async_trait { ... } fn supports_composed_reads(&self) -> bool { ... } fn invalidate_views<'life0, 'life1, 'async_trait>( &'life0 self, _views: &'life1 [ViewName], ) -> Pin<Box<dyn Future<Output = Result<u64, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait { ... } fn invalidate_by_entity<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _entity_type: &'life1 str, _entity_id: &'life2 str, ) -> Pin<Box<dyn Future<Output = Result<u64, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait { ... } fn result_cache_stats(&self) -> Option<ResultCacheStats> { ... } fn clear_result_cache<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<Option<usize>, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, Self: 'async_trait { ... } fn explain_query<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _sql: &'life1 str, _params: &'life2 [Value], ) -> Pin<Box<dyn Future<Output = Result<Value, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait { ... } fn explain_where_query<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _view: &'life1 str, _where_clause: Option<&'life2 WhereClause>, _limit: Option<u32>, _offset: Option<u32>, ) -> Pin<Box<dyn Future<Output = Result<Value, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait { ... } fn execute_where_query_arc_with_session<'life0, 'life1, 'life2, 'life3, 'life4, 'life5, 'life6, 'async_trait>( &'life0 self, view: &'life1 str, where_clause: Option<&'life2 WhereClause>, limit: Option<u32>, offset: Option<u32>, order_by: Option<&'life3 [OrderByClause]>, _session_vars: &'life4 [(&'life5 str, &'life6 str)], _routing: ReadRouting, ) -> Pin<Box<dyn Future<Output = Result<Arc<Vec<JsonbValue>>, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, 'life5: 'async_trait, 'life6: 'async_trait, Self: 'async_trait { ... } fn count_where_query<'life0, 'life1, 'life2, 'life3, 'life4, 'life5, 'async_trait>( &'life0 self, view: &'life1 str, where_clause: Option<&'life2 WhereClause>, session_vars: &'life3 [(&'life4 str, &'life5 str)], routing: ReadRouting, ) -> Pin<Box<dyn Future<Output = Result<u64, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, 'life5: 'async_trait, Self: 'async_trait { ... } fn execute_with_projection_arc_with_session<'life0, 'life1, 'life2, 'life3, 'life4, 'life5, 'async_trait>( &'life0 self, request: &'life1 ProjectionRequest<'life2>, _session_vars: &'life3 [(&'life4 str, &'life5 str)], _routing: ReadRouting, ) -> Pin<Box<dyn Future<Output = Result<Arc<Vec<JsonbValue>>, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, 'life5: 'async_trait, Self: 'async_trait { ... } fn stream_with_projection<'life0, 'life1, 'life2, 'life3, 'life4, 'life5, 'async_trait>( &'life0 self, request: &'life1 ProjectionRequest<'life2>, session_vars: &'life3 [(&'life4 str, &'life5 str)], routing: ReadRouting, ) -> Pin<Box<dyn Future<Output = Result<Pin<Box<dyn Stream<Item = Result<JsonbValue, FraiseQLError>> + Send>>, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, 'life5: 'async_trait, Self: 'async_trait { ... } fn execute_composed_with_session<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>( &'life0 self, read: &'life1 ComposedLevel, _session_vars: &'life2 [(&'life3 str, &'life4 str)], _routing: ReadRouting, ) -> Pin<Box<dyn Future<Output = Result<Arc<Vec<JsonbValue>>, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, Self: 'async_trait { ... } fn stream_row_query<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>( &'life0 self, view_name: &'life1 str, columns: &'life2 [ColumnSpec], where_sql: Option<&'life3 str>, order_by: Option<&'life4 str>, limit: Option<u32>, offset: Option<u32>, ) -> Pin<Box<dyn Future<Output = Result<Pin<Box<dyn Stream<Item = Result<Vec<ColumnValue>, FraiseQLError>> + Send>>, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, Self: 'async_trait { ... } fn stream_row_query_with_session<'life0, 'life1, 'life2, 'life3, 'life4, 'life5, 'life6, 'life7, 'async_trait>( &'life0 self, view_name: &'life1 str, columns: &'life2 [ColumnSpec], where_sql: Option<&'life3 str>, order_by: Option<&'life4 str>, limit: Option<u32>, offset: Option<u32>, _session_vars: &'life5 [(&'life6 str, &'life7 str)], ) -> Pin<Box<dyn Future<Output = Result<Pin<Box<dyn Stream<Item = Result<Vec<ColumnValue>, FraiseQLError>> + Send>>, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, 'life5: 'async_trait, 'life6: 'async_trait, 'life7: 'async_trait, Self: 'async_trait { ... } fn query_stats<'life0, 'async_trait>( &'life0 self, _limit: u32, ) -> Pin<Box<dyn Future<Output = Result<Vec<QueryStatEntry>, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, Self: 'async_trait { ... } fn query_stats_by_id<'life0, 'life1, 'async_trait>( &'life0 self, id: &'life1 str, ) -> Pin<Box<dyn Future<Output = Result<Option<QueryStatEntry>, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait { ... } fn reset_query_stats<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<(), FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, Self: 'async_trait { ... } fn on_schema_reload(&self) { ... } fn execute_admin_sql<'life0, 'life1, 'async_trait>( &'life0 self, _request: &'life1 AdminSqlRequest, ) -> Pin<Box<dyn Future<Output = Result<AdminSqlOutcome, FraiseQLError>> + Send + 'async_trait>> where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait { ... }
}
Expand description

Database adapter for executing queries against views.

This trait abstracts over different database backends (PostgreSQL, MySQL, SQLite, SQL Server). All implementations must support:

  • Executing parameterized WHERE queries against views
  • Returning JSONB data from the data column
  • Connection pooling and health checks
  • Row-level security (RLS) WHERE clauses

§Architecture

The adapter is the runtime interface to the database. It receives:

  • View/table name (e.g., “v_user”, “tf_sales”)
  • Parameterized WHERE clauses (AST form, not strings)
  • Projection hints (for performance optimization)
  • Pagination parameters (LIMIT/OFFSET)

And returns:

  • JSONB rows from the data column (most operations)
  • Arbitrary rows as HashMap (for aggregation queries)
  • Mutation results from stored procedures

§Implementing a New Adapter

To add support for a new database (e.g., Oracle, Snowflake):

  1. Create a new module in src/db/your_database/

  2. Implement the trait:

    ⓘ
    pub struct YourDatabaseAdapter { /* fields */ }
    
    #[async_trait]
    impl DatabaseAdapter for YourDatabaseAdapter {
        async fn execute_where_query(&self, ...) -> Result<Vec<JsonbValue>> {
            // 1. Build parameterized SQL from WhereClause AST
            // 2. Execute with bound parameters (NO string concatenation)
            // 3. Return JSONB from data column
        }
        // Implement other required methods...
    }
  3. Add feature flag to Cargo.toml (e.g., feature = "your-database")

  4. Copy structure from PostgreSQL adapter — see src/db/postgres/adapter.rs

  5. Add tests in tests/integration/your_database_test.rs

§Security Requirements

All implementations MUST:

  • Never concatenate user input into SQL strings
  • Always use parameterized queries with bind parameters
  • Validate parameter types before binding
  • Preserve RLS WHERE clauses (never filter them out)
  • Return errors, not silently fail (e.g., connection loss)

§Connection Management

  • Use a connection pool (recommended: 20 connections default)
  • Implement health_check() for ping-based monitoring
  • Provide pool_metrics() for observability
  • Handle stale connections gracefully

§Performance Characteristics

Expected throughput when properly implemented:

  • Simple queries (single table, no WHERE): 250+ Kelem/s
  • Complex queries (JOINs, multiple conditions): 50+ Kelem/s
  • Mutations (stored procedures): 1-10 RPS (depends on procedure)
  • Relay pagination (keyset cursors): 15-30ms latency

§Example: PostgreSQL Implementation

ⓘ
use sqlx::postgres::PgPool;
use async_trait::async_trait;

pub struct PostgresAdapter {
    pool: PgPool,
}

#[async_trait]
impl DatabaseAdapter for PostgresAdapter {
    async fn execute_where_query(
        &self,
        view: &str,
        where_clause: Option<&WhereClause>,
        limit: Option<u32>,
        offset: Option<u32>,
    ) -> Result<Vec<JsonbValue>> {
        // 1. Build SQL: SELECT data FROM {view} WHERE {where_clause} LIMIT {limit}
        let mut sql = format!(r#"SELECT data FROM "{}""#, view);

        // 2. Add WHERE clause (converts AST to parameterized SQL)
        let params = if let Some(where_clause) = where_clause {
            sql.push_str(" WHERE ");
            let (where_sql, params) = build_where_sql(where_clause)?;
            sql.push_str(&where_sql);
            params
        } else {
            vec![]
        };

        // 3. Add LIMIT and OFFSET
        if let Some(limit) = limit {
            sql.push_str(" LIMIT ");
            sql.push_str(&limit.to_string());
        }
        if let Some(offset) = offset {
            sql.push_str(" OFFSET ");
            sql.push_str(&offset.to_string());
        }

        // 4. Execute with bound parameters (NO string interpolation)
        let rows: Vec<(serde_json::Value,)> = sqlx::query_as(&sql)
            .bind(&params[0])
            .bind(&params[1])
            // ... bind all parameters
            .fetch_all(&self.pool)
            .await?;

        // 5. Extract JSONB and return
        Ok(rows.into_iter().map(|(data,)| data).collect())
    }

    // Implement other required methods...
}

§Example: Basic Usage

use fraiseql_db::{DatabaseAdapter, WhereClause, WhereOperator};
use serde_json::json;

// Build WHERE clause (AST, not string)
let where_clause = WhereClause::Field {
    path: vec!["email".to_string()],
    operator: WhereOperator::Icontains,
    value: json!("example.com"),
};

// Execute query with parameters
let results = adapter
    .execute_where_query("v_user", Some(&where_clause), Some(10), None, None)
    .await?;

println!("Found {} users matching filter", results.len());

§See Also

  • WhereClause — AST for parameterized WHERE clauses
  • RelayDatabaseAdapter — Optional trait for keyset pagination
  • Performance Guide

Required Methods§

Source

fn execute_where_query<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, view: &'life1 str, where_clause: Option<&'life2 WhereClause>, limit: Option<u32>, offset: Option<u32>, order_by: Option<&'life3 [OrderByClause]>, ) -> Pin<Box<dyn Future<Output = Result<Vec<JsonbValue>, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait,

Execute a WHERE query against a view and return JSONB rows.

§Arguments
  • view - View name (e.g., “v_user”, “v_post”)
  • where_clause - Optional WHERE clause AST
  • limit - Optional row limit (for pagination)
  • offset - Optional row offset (for pagination)
  • security_context - Optional security context for RLS and caching decisions
§Returns

Vec of JSONB values from the data column.

§Errors

Returns FraiseQLError::Database on query execution failure. Returns FraiseQLError::ConnectionPool if connection pool is exhausted.

§Example
// Simple query without WHERE clause
let all_users = adapter
    .execute_where_query("v_user", None, Some(10), Some(0), None)
    .await?;
Source

fn execute_with_projection<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>( &'life0 self, view: &'life1 str, projection: Option<&'life2 SqlProjectionHint>, where_clause: Option<&'life3 WhereClause>, limit: Option<u32>, offset: Option<u32>, order_by: Option<&'life4 [OrderByClause]>, ) -> Pin<Box<dyn Future<Output = Result<Vec<JsonbValue>, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, Self: 'async_trait,

Execute a WHERE query with SQL field projection optimization.

Projects only the requested fields at the database level, reducing network payload and JSON deserialization overhead by 40-55% based on production measurements.

This is the primary query execution method for optimized GraphQL queries. It automatically selects only the fields requested in the GraphQL query, avoiding unnecessary network transfer and deserialization of unused fields.

§Automatic Projection

In most cases, you don’t call this directly. The Executor automatically:

  1. Determines which fields the GraphQL query requests
  2. Generates a SqlProjectionHint using database-specific SQL
  3. Calls this method with the projection hint
§Arguments
  • view - View name (e.g., “v_user”, “v_post”)
  • projection - Optional SQL projection hint with field list
    • Some(hint): Use projection to select only requested fields
    • None: Falls back to standard query (full JSONB column)
  • where_clause - Optional WHERE clause AST for filtering
  • limit - Optional row limit (for pagination)
§Returns

Vec of JSONB values, either:

  • Full objects (when projection is None)
  • Projected objects with only requested fields (when projection is Some)
§Errors

Returns FraiseQLError::Database on query execution failure, including:

  • Connection pool exhaustion
  • SQL execution errors
  • Type mismatches
§Performance Characteristics

When projection is provided (recommended):

  • Latency: 40-55% reduction vs full object fetch
  • Network: 40-55% smaller payload (proportional to unused fields)
  • Throughput: Maintains 250+ Kelem/s (elements per second)
  • Memory: Proportional to projected fields only

Improvement scales with:

  • Percentage of unused fields (more unused = more improvement)
  • Size of result set (larger sets benefit more)
  • Network latency (network-bound queries benefit most)

When projection is None:

  • Behavior identical to execute_where_query()
  • Returns full JSONB column
  • Used for compatibility/debugging
§Database Support
DatabaseStatusImplementation
PostgreSQL✅ Optimizedjsonb_build_object()
MySQL⏳ FallbackServer-side filtering (planned)
SQLite⏳ FallbackServer-side filtering (planned)
SQL Server⏳ FallbackServer-side filtering (planned)
§Example: Direct Usage (Advanced)
// Requires: running PostgreSQL database and a DatabaseAdapter implementation.
use fraiseql_db::types::SqlProjectionHint;
use fraiseql_db::traits::DatabaseAdapter;
use fraiseql_db::DatabaseType;

let projection = SqlProjectionHint::new(
    DatabaseType::PostgreSQL,
    "jsonb_build_object(\
        'id', data->>'id', \
        'name', data->>'name', \
        'email', data->>'email'\
    )".to_string(),
    75,
);

let results = adapter
    .execute_with_projection("v_user", Some(&projection), None, Some(100), None, None)
    .await?;

// results only contain id, name, email fields
// 75% smaller than fetching all fields
§Example: Fallback (No Projection)
// Requires: running PostgreSQL database and a DatabaseAdapter implementation.
// For debugging or when projection not available
let results = adapter
    .execute_with_projection("v_user", None, None, Some(100), None, None)
    .await?;

// Equivalent to execute_where_query() - returns full objects
§See Also
  • execute_where_query() - Standard query without projection
  • SqlProjectionHint - Structure defining field projection
  • Projection Optimization Guide
Source

fn database_type(&self) -> DatabaseType

Get database type (for logging/metrics).

Used to identify which database backend is in use.

Source

fn health_check<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<(), FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, Self: 'async_trait,

Health check - verify database connectivity.

Executes a simple query (e.g., SELECT 1) to verify the database is reachable.

§Errors

Returns FraiseQLError::Database if health check fails.

Source

fn pool_metrics(&self) -> PoolMetrics

Get connection pool metrics.

Returns current statistics about the connection pool:

  • Total connections
  • Idle connections
  • Active connections
  • Waiting requests
Source

fn execute_raw_query<'life0, 'life1, 'async_trait>( &'life0 self, sql: &'life1 str, ) -> Pin<Box<dyn Future<Output = Result<Vec<HashMap<String, Value>>, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait,

Execute raw SQL query and return rows as JSON objects.

Used for aggregation queries where we need full row data, not just JSONB column.

§Security Warning

This method executes arbitrary SQL. NEVER pass untrusted input directly to this method. Always:

  • Use parameterized queries with bound parameters
  • Validate and sanitize SQL templates before execution
  • Only execute SQL generated by the FraiseQL compiler
  • Log SQL execution for audit trails
§Arguments
  • sql - Raw SQL query to execute (must be safe/trusted)
§Returns

Vec of rows, where each row is a HashMap of column name to JSON value.

§Errors

Returns FraiseQLError::Database on query execution failure.

§Example
// Safe: SQL generated by FraiseQL compiler
let sql = "SELECT category, SUM(revenue) as total FROM tf_sales GROUP BY category";
let rows = adapter.execute_raw_query(sql).await?;
for row in rows {
    println!("Category: {}, Total: {}", row["category"], row["total"]);
}
Source

fn execute_parameterized_aggregate<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, sql: &'life1 str, params: &'life2 [Value], ) -> Pin<Box<dyn Future<Output = Result<Vec<HashMap<String, Value>>, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait,

Execute a parameterized aggregate SQL query (GROUP BY / HAVING / window).

sql contains $N (PostgreSQL), ? (MySQL / SQLite), or @P1 (SQL Server) placeholders for string and array values; numeric and NULL values may be inlined. params are the corresponding values in placeholder order.

Unlike execute_raw_query, this method accepts bind parameters so that user-supplied filter values never appear as string literals in the SQL text, eliminating the injection risk that escape_sql_string mitigated previously.

§Arguments
  • sql - SQL with placeholders generated by AggregationSqlGenerator::generate_parameterized
  • params - Bind parameters in placeholder order
§Returns

Vec of rows, where each row is a HashMap of column name to JSON value.

§Errors

Returns FraiseQLError::Database on execution failure. Returns FraiseQLError::Database on adapters that do not support raw SQL (e.g., FraiseWireAdapter).

Provided Methods§

Source

fn execute_where_query_arc<'life0, 'life1, 'life2, 'life3, 'async_trait>( &'life0 self, view: &'life1 str, where_clause: Option<&'life2 WhereClause>, limit: Option<u32>, offset: Option<u32>, order_by: Option<&'life3 [OrderByClause]>, ) -> Pin<Box<dyn Future<Output = Result<Arc<Vec<JsonbValue>>, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, Self: 'async_trait,

Like execute_where_query but returns the result wrapped in an Arc.

The default implementation wraps the result of execute_where_query in a fresh Arc. CachedDatabaseAdapter overrides this to return the cached Arc directly — eliminating the full Vec<JsonbValue> clone that the non-Arc path requires on every cache hit.

Callers on the hot query path should prefer this variant and borrow from the Arc via &**arc rather than taking ownership.

§Errors

Same errors as execute_where_query.

Source

fn execute_with_projection_arc<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, request: &'life1 ProjectionRequest<'life2>, ) -> Pin<Box<dyn Future<Output = Result<Arc<Vec<JsonbValue>>, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait,

Like execute_with_projection but returns the result wrapped in an Arc.

The default implementation wraps the result of execute_with_projection in a fresh Arc. CachedDatabaseAdapter overrides this to return the cached Arc directly — eliminating the full Vec<JsonbValue> clone that the non-Arc path requires on every cache hit.

Parameters are passed in a ProjectionRequest struct (F043) so adapters and callers cannot misorder them.

§Errors

Same errors as execute_with_projection.

Source

fn execute_row_query<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>( &'life0 self, view_name: &'life1 str, columns: &'life2 [ColumnSpec], where_sql: Option<&'life3 str>, order_by: Option<&'life4 str>, limit: Option<u32>, offset: Option<u32>, ) -> Pin<Box<dyn Future<Output = Result<Vec<Vec<ColumnValue>>, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, Self: 'async_trait,

Execute a row-shaped query against a view, returning typed column values.

Used by the gRPC transport for protobuf encoding of query results. The default implementation delegates to execute_raw_query and converts JSON results to ColumnValue vectors.

§Errors

Returns FraiseQLError::Database if the adapter returns an error.

Source

fn execute_row_query_with_session<'life0, 'life1, 'life2, 'life3, 'life4, 'life5, 'life6, 'life7, 'async_trait>( &'life0 self, view_name: &'life1 str, columns: &'life2 [ColumnSpec], where_sql: Option<&'life3 str>, order_by: Option<&'life4 str>, limit: Option<u32>, offset: Option<u32>, _session_vars: &'life5 [(&'life6 str, &'life7 str)], ) -> Pin<Box<dyn Future<Output = Result<Vec<Vec<ColumnValue>>, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, 'life5: 'async_trait, 'life6: 'async_trait, 'life7: 'async_trait, Self: 'async_trait,

Connection-affine variant of execute_row_query.

See execute_where_query_arc_with_session for the rationale: an RLS policy backed by current_setting() (#329) only sees the variables if they are applied transaction-locally on the same connection as the read.

§Why the row shape needs its own pair

The row-shaped read is the gRPC transport’s source. Until #1351 it resolved its own predicate and never reached the engine, so it had no session variables to pin and none of these methods existed. Routing it through the direct-read chokepoint gives it session_variables like every other read — and a resolved value the read cannot apply is the failure mode the chokepoint exists to prevent, so it travels rather than being dropped.

§Errors

Same errors as execute_row_query; additionally returns FraiseQLError::Database if set_config fails on any pair.

Source

fn execute_parameterized_aggregate_with_session<'life0, 'life1, 'life2, 'life3, 'life4, 'life5, 'async_trait>( &'life0 self, sql: &'life1 str, params: &'life2 [Value], _session_vars: &'life3 [(&'life4 str, &'life5 str)], _routing: ReadRouting, ) -> Pin<Box<dyn Future<Output = Result<Vec<HashMap<String, Value>>, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, 'life5: 'async_trait, Self: 'async_trait,

Connection-affine variant of execute_parameterized_aggregate.

Applies session_vars transaction-locally on the same connection that runs the aggregate, so aggregate views backed by current_setting() RLS observe the configured values (fixes #329 for the aggregate path). The default ignores session_vars: a backend without transaction-local settings has none to apply.

§Errors

Same errors as execute_parameterized_aggregate; additionally returns FraiseQLError::Database if set_config fails.

Source

fn supports_composed_reads(&self) -> bool

Whether this adapter implements execute_composed_with_session — whether a read can have related resources composed into it (the REST ?select= embed).

Asked twice, of one answer: when the REST transport is mounted, so a deployment whose adapter cannot serve embeds is told at boot rather than by its first embed; and by the engine before a composed read is sent, so the 501 a request gets is that same fact rather than a second one.

Implementing execute_composed_with_session obliges you to override this too, and a wrapping adapter must forward both. Defaults to false: a capability is not something a backend should acquire by omission.

Source

fn invalidate_views<'life0, 'life1, 'async_trait>( &'life0 self, _views: &'life1 [ViewName], ) -> Pin<Box<dyn Future<Output = Result<u64, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait,

Invalidate cached query results for the specified views.

Called by the executor after a mutation succeeds, so that stale cache entries reading from modified views are evicted. The default implementation is a no-op; CachedDatabaseAdapter overrides this.

View names are passed as &[ViewName] so the wrapper’s Arc<str> backing is preserved across the call. Callers that hold a String can convert in place with ViewName::from(...).

§Returns

The number of cache entries evicted.

Source

fn invalidate_by_entity<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _entity_type: &'life1 str, _entity_id: &'life2 str, ) -> Pin<Box<dyn Future<Output = Result<u64, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait,

Evict cache entries that contain the given entity UUID.

Called by the executor after a successful UPDATE or DELETE mutation when the mutation_response includes an entity_id. Only cache entries whose entity-ID index contains the given UUID are removed; unrelated entries remain warm.

The default implementation is a no-op. CachedDatabaseAdapter overrides this to perform the selective eviction.

§Returns

The number of cache entries evicted.

Source

fn result_cache_stats(&self) -> Option<ResultCacheStats>

A snapshot of this adapter’s query-result cache, or None if it has none.

The operator surface for the cache that actually serves GraphQL queries. None means “this adapter does not cache” — distinct from Some(stats) with zero entries, which means “the cache is there and currently empty”. The admin API reported the second as the first for every non-Arrow deployment (#941).

The default implementation returns None; CachedDatabaseAdapter overrides it.

Source

fn clear_result_cache<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<Option<usize>, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, Self: 'async_trait,

Drop every entry in this adapter’s query-result cache.

Returns the number of entries dropped, or None if this adapter has no cache — again so the caller can say “there is no such cache” rather than reporting a successful clear of nothing.

The default implementation returns Ok(None); CachedDatabaseAdapter overrides it.

§Errors

Returns FraiseQLError if the underlying cache cannot be cleared.

Source

fn explain_query<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _sql: &'life1 str, _params: &'life2 [Value], ) -> Pin<Box<dyn Future<Output = Result<Value, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait,

Run the database’s EXPLAIN on a SQL statement without executing it.

Returns a JSON representation of the query plan. The format is database-specific (e.g. PostgreSQL returns JSON, SQLite returns rows).

The default implementation returns Unsupported.

Source

fn explain_where_query<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, _view: &'life1 str, _where_clause: Option<&'life2 WhereClause>, _limit: Option<u32>, _offset: Option<u32>, ) -> Pin<Box<dyn Future<Output = Result<Value, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, Self: 'async_trait,

Run EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) against a view with the same parameterized WHERE clause that execute_where_query would use.

Unlike explain_query, this method uses real bound parameters and actually executes the query (ANALYZE mode), so the plan reflects PostgreSQL’s runtime statistics for the given filter values.

Only PostgreSQL supports this; other adapters return FraiseQLError::Unsupported by default.

§Arguments
  • view - View name (e.g., “v_user”)
  • where_clause - Optional filter (same as execute_where_query)
  • limit - Optional row limit
  • offset - Optional row offset
§Errors

Returns FraiseQLError::Database on execution failure. Returns FraiseQLError::Unsupported for non-PostgreSQL adapters.

Source

fn execute_where_query_arc_with_session<'life0, 'life1, 'life2, 'life3, 'life4, 'life5, 'life6, 'async_trait>( &'life0 self, view: &'life1 str, where_clause: Option<&'life2 WhereClause>, limit: Option<u32>, offset: Option<u32>, order_by: Option<&'life3 [OrderByClause]>, _session_vars: &'life4 [(&'life5 str, &'life6 str)], _routing: ReadRouting, ) -> Pin<Box<dyn Future<Output = Result<Arc<Vec<JsonbValue>>, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, 'life5: 'async_trait, 'life6: 'async_trait, Self: 'async_trait,

Connection-affine variant of execute_where_query_arc.

Applies session_vars transaction-locally on the same connection that runs the read, so PostgreSQL Row-Level-Security policies backed by current_setting() see the configured values (fixes #329). The default ignores session_vars: a backend without transaction-local settings has none to apply.

§Errors

Same errors as execute_where_query_arc; additionally returns FraiseQLError::Database if set_config fails on any pair.

Source

fn count_where_query<'life0, 'life1, 'life2, 'life3, 'life4, 'life5, 'async_trait>( &'life0 self, view: &'life1 str, where_clause: Option<&'life2 WhereClause>, session_vars: &'life3 [(&'life4 str, &'life5 str)], routing: ReadRouting, ) -> Pin<Box<dyn Future<Output = Result<u64, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, 'life5: 'async_trait, Self: 'async_trait,

Count the rows of view matching where_clause, without materialising them.

Backs both the GraphQL <name>Count sibling (#938) and the REST Prefer: count=exact header, so the two cannot answer the same question differently.

session_vars are applied transaction-locally on the connection that runs the count, exactly as for the read it describes (#329). This is load-bearing rather than symmetric-for-neatness: under RLS, a count that ran without the session variables would report the unfiltered total beside a filtered page — a row-count oracle over rows the caller cannot read.

§Default implementation

The default fetches the matching rows and counts them. That is correct — same view, same predicate, same session — but it is O(rows) in memory, so an adapter that can push the count into the database should override it. PostgresAdapter does, with SELECT COUNT(*).

§Errors

Returns FraiseQLError::Database if the query fails.

Source

fn execute_with_projection_arc_with_session<'life0, 'life1, 'life2, 'life3, 'life4, 'life5, 'async_trait>( &'life0 self, request: &'life1 ProjectionRequest<'life2>, _session_vars: &'life3 [(&'life4 str, &'life5 str)], _routing: ReadRouting, ) -> Pin<Box<dyn Future<Output = Result<Arc<Vec<JsonbValue>>, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, 'life5: 'async_trait, Self: 'async_trait,

Connection-affine variant of execute_with_projection_arc.

See execute_where_query_arc_with_session for the rationale.

§Errors

Same errors as execute_with_projection_arc; additionally returns FraiseQLError::Database if set_config fails on any pair.

Source

fn stream_with_projection<'life0, 'life1, 'life2, 'life3, 'life4, 'life5, 'async_trait>( &'life0 self, request: &'life1 ProjectionRequest<'life2>, session_vars: &'life3 [(&'life4 str, &'life5 str)], routing: ReadRouting, ) -> Pin<Box<dyn Future<Output = Result<Pin<Box<dyn Stream<Item = Result<JsonbValue, FraiseQLError>> + Send>>, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, 'life5: 'async_trait, Self: 'async_trait,

The same read as execute_with_projection_arc_with_session, delivered row by row instead of as a materialised Vec (#958).

Same view, same predicate, same session variables, same routing. The only difference is when the rows arrive and how much memory the caller needs to hold them: a streamed read is O(1) in the number of rows, so an export of a table larger than memory is possible where the collecting method would have to fail.

§Why this exists as its own method

A paginated re-execution loop (LIMIT n OFFSET k, k += n) delivers the same rows in bounded memory and needs no new trait method, and that is what the export surfaces did. It has two properties a stream does not:

  • it is O(offset) per batch, so exporting N rows costs O(N²) row scans;
  • each batch is its own snapshot, so a concurrent insert or delete between batches shifts rows across the page boundary — silently duplicating a row into two batches, or skipping one entirely.

One statement over one portal has neither problem.

§Default implementation

Collects the read and replays it. That is correct — same rows, same order — but it holds the whole result set, so an adapter that can stream should override it. PostgresAdapter does.

⚠ A wrapping adapter (caching, instrumentation) that inherits this default converts every streaming caller back into a buffering one without failing: the rows are right and the memory bound is gone. A wrapper must forward this method explicitly.

§Errors

Returns the same errors as execute_with_projection_arc_with_session. Note that an implementation that truly streams can only report the errors it has seen so far when this returns: a failure raised after the first row — a lost connection, a runtime cast error on a later row — surfaces as an Err item inside the stream, not from this call.

Source

fn execute_composed_with_session<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>( &'life0 self, read: &'life1 ComposedLevel, _session_vars: &'life2 [(&'life3 str, &'life4 str)], _routing: ReadRouting, ) -> Pin<Box<dyn Future<Output = Result<Arc<Vec<JsonbValue>>, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, Self: 'async_trait,

Execute a read that composes its embedded levels into one statement.

The REST ?select= embed used to be a parent read plus one sub-read per parent row per level; this is the same answer in one round trip, one snapshot and one pooled connection. See ComposedLevel for the call shape and for the {"d": …, "e": …} rows it returns.

Session variables and routing mean what they mean for execute_with_projection_arc_with_session.

§Default implementation

Refuses. Composing needs correlated LATERAL subqueries and JSON aggregation, which an adapter has to render for its own dialect; answering with anything else — the root rows without their embeds, say — would be a partial response indistinguishable from a parent with no related rows. A wrapping adapter must forward this method explicitly.

§Errors

FraiseQLError::Unsupported from the default; otherwise the errors of execute_with_projection_arc_with_session.

Source

fn stream_row_query<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>( &'life0 self, view_name: &'life1 str, columns: &'life2 [ColumnSpec], where_sql: Option<&'life3 str>, order_by: Option<&'life4 str>, limit: Option<u32>, offset: Option<u32>, ) -> Pin<Box<dyn Future<Output = Result<Pin<Box<dyn Stream<Item = Result<Vec<ColumnValue>, FraiseQLError>> + Send>>, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, Self: 'async_trait,

The same read as execute_row_query, delivered row by row (#958).

The column-shaped counterpart of stream_with_projection; see that method for why streaming and paginated re-execution are not interchangeable. This is the gRPC transport’s source, which encodes one protobuf message per row and has no use for a Vec of them.

§Default implementation

Collects and replays, with the same caveat about wrapping adapters.

§Errors

Returns the same errors as execute_row_query; see stream_with_projection for which of them can still arrive after this call has returned Ok.

Source

fn stream_row_query_with_session<'life0, 'life1, 'life2, 'life3, 'life4, 'life5, 'life6, 'life7, 'async_trait>( &'life0 self, view_name: &'life1 str, columns: &'life2 [ColumnSpec], where_sql: Option<&'life3 str>, order_by: Option<&'life4 str>, limit: Option<u32>, offset: Option<u32>, _session_vars: &'life5 [(&'life6 str, &'life7 str)], ) -> Pin<Box<dyn Future<Output = Result<Pin<Box<dyn Stream<Item = Result<Vec<ColumnValue>, FraiseQLError>> + Send>>, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait, 'life3: 'async_trait, 'life4: 'async_trait, 'life5: 'async_trait, 'life6: 'async_trait, 'life7: 'async_trait, Self: 'async_trait,

Connection-affine variant of stream_row_query.

The streaming twin of execute_row_query_with_session; see that method for why the row shape carries session variables at all. The streaming arm is where dropping them costs the most, because it is the arm that delivers an unbounded number of rows.

§Errors

Same errors as stream_row_query; additionally returns FraiseQLError::Database if set_config fails on any pair.

Source

fn query_stats<'life0, 'async_trait>( &'life0 self, _limit: u32, ) -> Pin<Box<dyn Future<Output = Result<Vec<QueryStatEntry>, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, Self: 'async_trait,

Retrieve query performance statistics from the database.

Returns the top-N queries ordered by total execution time (descending). The exact data source depends on the backend:

  • PostgreSQL: pg_stat_statements (requires extension)
  • MySQL: performance_schema.events_statements_summary_by_digest
  • SQL Server: sys.dm_exec_query_stats
  • SQLite / Wire: empty (no stats available)
§Arguments
  • limit - Maximum number of entries to return.
§Errors

Returns FraiseQLError::Database if the stats query fails.

Source

fn query_stats_by_id<'life0, 'life1, 'async_trait>( &'life0 self, id: &'life1 str, ) -> Pin<Box<dyn Future<Output = Result<Option<QueryStatEntry>, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait,

Retrieve statistics for a single query by its ID.

The default implementation fetches up to 1000 entries via query_stats and filters client-side. Backends with efficient single-query lookup (PostgreSQL, SQL Server) should override with a WHERE clause.

§Errors

Returns FraiseQLError::Database if the underlying query fails.

Source

fn reset_query_stats<'life0, 'async_trait>( &'life0 self, ) -> Pin<Box<dyn Future<Output = Result<(), FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, Self: 'async_trait,

Reset query performance statistics.

Only PostgreSQL supports this (via pg_stat_statements_reset()). All other adapters return Unsupported.

§Errors

Returns FraiseQLError::Unsupported for adapters that cannot reset stats. Returns FraiseQLError::Database if the reset command fails.

Source

fn on_schema_reload(&self)

Notify the adapter that the schema has changed.

Called during hot-reload after the new schema has been validated. Adapters that maintain schema-dependent state (e.g. cache keyed by schema version) should clear or rebuild that state here.

The default implementation is a no-op.

Source

fn execute_admin_sql<'life0, 'life1, 'async_trait>( &'life0 self, _request: &'life1 AdminSqlRequest, ) -> Pin<Box<dyn Future<Output = Result<AdminSqlOutcome, FraiseQLError>> + Send + 'async_trait>>
where 'life0: 'async_trait, 'life1: 'async_trait, Self: 'async_trait,

Run one operator-supplied statement under the bounds in request (#962).

This is the only method on this trait whose SQL FraiseQL did not generate. It exists for the Studio SQL console, and it is deliberately the narrowest possible shape for that: a single statement, in a transaction whose mode, timeout, row budget and session identity are all decided by the caller and enforced by the database. See AdminSqlRequest for what each bound buys.

§Default implementation

Refuses. An adapter that has not implemented containment does not get to run arbitrary SQL by inheriting a default that “just works” — the convenient default here would be execute_raw_query, which has no transaction, no timeout, no row cap and no way to roll back. Every adapter that should serve this endpoint says so by overriding, and the endpoint answers “unsupported” for the rest.

A wrapping adapter must forward explicitly for the same reason it forwards stream_with_projection: inheriting the default here turns a capable adapter into an incapable one, and the only symptom is a refusal nobody asked for.

§Errors

Returns FraiseQLError::Unsupported by default. Implementations return FraiseQLError::Database for anything PostgreSQL rejects — which includes the write refused by a READ ONLY transaction (SQLSTATE 25006) and the statement cancelled by the timeout (57014).

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§