pub struct Executor { /* private fields */ }Expand description
Query executor - executes compiled GraphQL queries.
This is the main entry point for runtime query execution. It coordinates matching, planning, execution, and projection.
§The adapter has no name here
Executor is not generic over its adapter. The concrete type is consumed by the
constructor and stored erased, as Arc<dyn DatabaseAdapter>, so a holder of an
Executor cannot name the adapter, cannot recover it, and cannot call anything on
it that the engine does not offer. That is the whole point: a transport handed an
executor is handed the engine’s surface, not a database handle it can go around the
engine with.
The capability that the type parameter used to carry is carried by a value instead.
A constructor bounded on Writer holds the adapter as its write handle; everything
else holds None and refuses every write. See Executor::new and
Executor::read_only.
§Ownership and Lifetimes
The executor holds owned references to schema and runtime data, with no borrowed pointers:
schema: OwnedCompiledSchema(immutable after construction)adapter: Shared viaArc<dyn DatabaseAdapter>so multiple executors and tasks use the same connection poolintrospection: Owned cached GraphQL schema responsesconfig: Owned runtime configuration
No explicit lifetimes required - all data is either owned or wrapped in Arc,
so the executor can be stored in long-lived structures without lifetime annotations or
borrow-checker issues.
§Concurrency
Executor is Send + Sync — DatabaseAdapter requires both of every implementor, so
erasing the adapter cannot lose them. It can be safely shared across threads and tasks
without cloning:
// Requires: a live database adapter.
// See: tests/integration/ for runnable examples.
// let executor = Arc::new(Executor::new(schema, adapter));
// Can be cloned into multiple tasks
// let exec_clone = executor.clone();
// tokio::spawn(async move {
// let result = exec_clone.execute(query, vars).await;
// });§Query Timeout
Queries are protected by the query_timeout_ms configuration in RuntimeConfig (default: 30s).
When a query exceeds this timeout, it returns FraiseQLError::Timeout without panicking.
Set query_timeout_ms to 0 to disable timeout enforcement.
Implementations§
Source§impl Executor
Compile-time enforcement lives here, on the constructor.
impl Executor
Compile-time enforcement lives here, on the constructor.
The bound on this block is what keeps a read-only adapter from ever being handed a
write handle. Its witness is FraiseWireAdapter, which implements DatabaseAdapter
and deliberately not Writer.
⚠ The two blocks below are a pair, and only the pair is the assertion. A
compile_fail block is satisfied by any compile error, including one with nothing
to do with the rule — the previous version of this pair named SqliteAdapter, and
when #374 deleted that adapter the block passed for a full release while proving
nothing. The two differ by exactly the constructor called. If the witness ever stops
resolving, the first block goes red rather than the second going quietly green.
FraiseWireAdapter needs --all-features; every --doc invocation in this
repository passes it (Makefile, .dagger/main.go).
The witness resolves, and the read-only constructor accepts it:
use fraiseql_core::{db::FraiseWireAdapter, runtime::Executor, schema::CompiledSchema};
use std::sync::Arc;
fn _read_only_is_available(schema: CompiledSchema, adapter: Arc<FraiseWireAdapter>) {
let _ = Executor::read_only(schema, adapter);
}…and the write-capable constructor does not compile for it:
use fraiseql_core::{db::FraiseWireAdapter, runtime::Executor, schema::CompiledSchema};
use std::sync::Arc;
fn _wont_compile(schema: CompiledSchema, adapter: Arc<FraiseWireAdapter>) {
let _ = Executor::new(schema, adapter);
}Sourcepub fn new<A: Writer>(schema: CompiledSchema, adapter: Arc<A>) -> Self
pub fn new<A: Writer>(schema: CompiledSchema, adapter: Arc<A>) -> Self
Create a new write-capable executor.
Available only for adapters that implement Writer — implementing its one
method is the write capability (ruling AA 6). For one that does not — or one whose
write capability you do not want to grant — use read_only,
which is bounded only on DatabaseAdapter.
§Arguments
schema- Compiled schemaadapter- Database adapter
§Example
// Requires: a live PostgreSQL database.
// See: tests/integration/ for runnable examples.
let schema = CompiledSchema::from_json(schema_json, false)?;
let adapter = PostgresAdapter::new(connection_string).await?;
let executor = Executor::new(schema, Arc::new(adapter));Sourcepub fn with_config<A: Writer>(
schema: CompiledSchema,
adapter: Arc<A>,
config: RuntimeConfig,
) -> Self
pub fn with_config<A: Writer>( schema: CompiledSchema, adapter: Arc<A>, config: RuntimeConfig, ) -> Self
Create a new write-capable executor with custom configuration.
§Arguments
schema- Compiled schemaadapter- Database adapterconfig- Runtime configuration
Sourcepub fn read_only<A: DatabaseAdapter>(
schema: CompiledSchema,
adapter: Arc<A>,
) -> Self
pub fn read_only<A: DatabaseAdapter>( schema: CompiledSchema, adapter: Arc<A>, ) -> Self
Create a new executor that cannot write.
The entry for an adapter that is not a Writer — a read-replica handle,
FraiseWireAdapter, a read-only test double — and for a write-capable adapter you
want to expose read-only. Mutations are refused on every transport, because the
refusal is the absence of a handle rather than a check somebody has to remember.
§Arguments
schema- Compiled schemaadapter- Database adapter
Sourcepub fn read_only_with_config<A: DatabaseAdapter>(
schema: CompiledSchema,
adapter: Arc<A>,
config: RuntimeConfig,
) -> Self
pub fn read_only_with_config<A: DatabaseAdapter>( schema: CompiledSchema, adapter: Arc<A>, config: RuntimeConfig, ) -> Self
Create a new read-only executor with custom configuration.
§Arguments
schema- Compiled schemaadapter- Database adapterconfig- Runtime configuration
Sourcepub fn pool_metrics(&self) -> PoolMetrics
pub fn pool_metrics(&self) -> PoolMetrics
Return current connection pool metrics from the underlying database adapter.
Values are sampled live on each call — not cached — so callers (e.g., the
/metrics endpoint) always observe up-to-date pool health.
Sourcepub fn max_query_depth(&self) -> u32
pub fn max_query_depth(&self) -> u32
How deep a selection may nest on this executor: its depth gate’s
max_query_depth — declared, installed, or DEFAULT_MAX_QUERY_DEPTH. Every
resolution of a document’s selection set refuses past it, including the
transport’s own (the SSE planner), so no path answers depth differently.
Sourcepub fn schema(&self) -> &CompiledSchema
pub fn schema(&self) -> &CompiledSchema
Get the compiled schema.
Sourcepub fn config(&self) -> &RuntimeConfig
pub fn config(&self) -> &RuntimeConfig
Get runtime configuration.
Sourcepub fn relay_enabled(&self) -> bool
pub fn relay_enabled(&self) -> bool
Whether this executor can dispatch relay (cursor) pagination.
true only for executors built by Executor::new_with_relay or
Executor::with_config_and_relay; a relay query issued against any
other executor returns a Validation error. Exposed so a rebuild of the
executor — a hot-reload, a per-tenant provision — can assert it preserved
the capability rather than silently downgrading it (#750).
Sourcepub fn database_type(&self) -> DatabaseType
pub fn database_type(&self) -> DatabaseType
Which backend this executor is bound to.
Sourcepub async fn health_check(&self) -> Result<()>
pub async fn health_check(&self) -> Result<()>
Sourcepub async fn query_stats(&self, limit: u32) -> Result<Vec<QueryStatEntry>>
pub async fn query_stats(&self, limit: u32) -> Result<Vec<QueryStatEntry>>
The slowest limit statements the backend is willing to report.
§Errors
Returns FraiseQLError::Unsupported on a backend with no statement-stats
facility, or the backend’s own error when the read fails.
Sourcepub async fn query_stats_by_id(
&self,
id: &str,
) -> Result<Option<QueryStatEntry>>
pub async fn query_stats_by_id( &self, id: &str, ) -> Result<Option<QueryStatEntry>>
Sourcepub async fn reset_query_stats(&self) -> Result<()>
pub async fn reset_query_stats(&self) -> Result<()>
Sourcepub fn result_cache_stats(&self) -> Option<ResultCacheStats>
pub fn result_cache_stats(&self) -> Option<ResultCacheStats>
Adapter-level result-cache counters, or None when no cache is active.
Sourcepub async fn clear_result_cache(&self) -> Result<Option<usize>>
pub async fn clear_result_cache(&self) -> Result<Option<usize>>
Evict every entry from the adapter-level result cache.
Ok(None) means the backend has no such cache to clear.
§Errors
Returns the backend’s own error when eviction fails.
Sourcepub async fn invalidate_views(&self, views: &[ViewName]) -> Result<u64>
pub async fn invalidate_views(&self, views: &[ViewName]) -> Result<u64>
Evict adapter-level result-cache entries derived from the given views.
Returns the number of entries removed; 0 on a backend with no such cache.
§Errors
Returns the backend’s own error when eviction fails.
Sourcepub async fn explain_query(&self, sql: &str, params: &[Value]) -> Result<Value>
pub async fn explain_query(&self, sql: &str, params: &[Value]) -> Result<Value>
The backend’s plan for a statement, as its own JSON shape.
§Errors
Returns FraiseQLError::Unsupported on a backend with no EXPLAIN, or the
backend’s own error when planning fails.
Sourcepub async fn execute_admin_sql(
&self,
request: &AdminSqlRequest,
) -> Result<AdminSqlOutcome>
pub async fn execute_admin_sql( &self, request: &AdminSqlRequest, ) -> Result<AdminSqlOutcome>
Run the admin-SQL route’s bounded statement.
§Errors
Returns FraiseQLError::Unsupported on a backend that declines admin SQL, or
the backend’s own error when the statement fails.
Sourcepub fn on_schema_reload(&self)
pub fn on_schema_reload(&self)
Tell the backend the compiled schema changed, so it can drop anything it derived from the old one — a result cache keyed by the old views, say.
Call this before Executor::rebuild_with: the backend is shared between
the old executor and the new one, so anything stale it holds would otherwise
outlive the swap.
Sourcepub async fn refuse_standby_unreadable_sources(
&self,
schema: &CompiledSchema,
) -> Result<()>
pub async fn refuse_standby_unreadable_sources( &self, schema: &CompiledSchema, ) -> Result<()>
Refuse schema when this executor’s reads go to hot standbys and a source of it
depends on an UNLOGGED or temporary table (#1390).
The boot-time check, re-run by a hot reload before it swaps: a query added by a reload reads through the same replicas the server was started with.
§Errors
Sourcepub async fn drop_tenant_schema(&self, schema_name: &str) -> Result<()>
pub async fn drop_tenant_schema(&self, schema_name: &str) -> Result<()>
Drop a tenant’s PostgreSQL schema and everything in it.
Takes the schema name, not a statement: the engine composes the DDL, so no
caller-supplied SQL reaches the backend through this door. Deleting
Executor::adapter removed the transports’ general raw-SQL reach, and this is
deliberately not a replacement for it — it does one thing.
The name is re-validated here even though fraiseql-server validates the
tenant key before deriving it. This is the interpolation site, so it is the
site that has to be safe on its own; a guard that holds only while every
caller remembers to validate is not a guard.
§Errors
Returns FraiseQLError::Validation if schema_name is not a bare identifier
(ASCII alphanumeric and underscore, non-empty, at most 63 bytes), and the
backend’s own error if the DDL fails.
Sourcepub fn rebuild_with(
&self,
schema: CompiledSchema,
config: RuntimeConfig,
) -> Self
pub fn rebuild_with( &self, schema: CompiledSchema, config: RuntimeConfig, ) -> Self
Build a new executor over this executor’s backend, for a hot-reload or a re-provision.
Supersedes the recorded-rebuilder closure of #750. That closure existed
because relay dispatch needs a RelayDatabaseAdapter bound that is only in
scope at the relay constructor, so a rebuild had to re-run the constructor
that had it — and the recording could be wrong, which is what #750 guarded
against by hand. Nothing needs re-running: RelayDispatchImpl holds the
adapter and nothing schema-derived, so the same dispatch object is still
correct for the new schema and is carried over as-is. A rebuild cannot
downgrade a relay executor to a non-relay one, because there is no longer a
step that could omit it.
The write grant is carried over for the same reason and in the same way. It is resolved from the adapter, and the adapter is unchanged, so recomputing it could only ever agree — but a rebuild that recomputed a capability is precisely the step #750 showed can omit one. Carrying it makes a downgrade unrepresentable rather than merely unlikely. The refusal beside it is recomputed from the same adapter, so it agrees with the boot (ruling Z 1).
Sourcepub async fn execute_aggregate_query(
&self,
query_json: &Value,
query_name: &str,
metadata: &FactTableMetadata,
) -> Result<Value>
pub async fn execute_aggregate_query( &self, query_json: &Value, query_name: &str, metadata: &FactTableMetadata, ) -> Result<Value>
Execute an aggregate query directly.
§Errors
Returns error if query parsing, plan generation, SQL generation, database execution, or result projection fails.
Sourcepub async fn execute_window_query(
&self,
query_json: &Value,
query_name: &str,
metadata: &FactTableMetadata,
) -> Result<Value>
pub async fn execute_window_query( &self, query_json: &Value, query_name: &str, metadata: &FactTableMetadata, ) -> Result<Value>
Execute a window query directly.
§Errors
Returns error if query parsing, plan generation, SQL generation, database execution, or result projection fails.
Sourcepub async fn count_rows(
&self,
query_match: &QueryMatch,
variables: Option<&Value>,
security_context: Option<&SecurityContext>,
) -> Result<u64>
pub async fn count_rows( &self, query_match: &QueryMatch, variables: Option<&Value>, security_context: Option<&SecurityContext>, ) -> Result<u64>
Count rows matching a query’s filters.
Delegates to QueryRunner::count_rows.
§Errors
Returns FraiseQLError::Validation if the query has no SQL source, or if
inject params are required but no security context is provided.
Returns FraiseQLError::Database if the adapter returns an error.
Sourcepub async fn execute_query_direct(
&self,
query_match: &QueryMatch,
variables: Option<&Value>,
security_context: Option<&SecurityContext>,
request_budget: Option<&RequestBudget>,
) -> Result<Value>
pub async fn execute_query_direct( &self, query_match: &QueryMatch, variables: Option<&Value>, security_context: Option<&SecurityContext>, request_budget: Option<&RequestBudget>, ) -> Result<Value>
Execute a pre-resolved query match directly, bypassing GraphQL parsing.
Used by the REST transport after route resolution: the QueryMatch is
already computed from HTTP path/query parameters, so there is no need
to re-parse a GraphQL query string.
§Errors
Returns FraiseQLError::Validation if the query has no SQL source.
Returns FraiseQLError::Database if the adapter returns an error.
Sourcepub fn supports_composed_reads(&self) -> bool
pub fn supports_composed_reads(&self) -> bool
Whether this executor’s database adapter can compose related resources into a read
(execute_query_composed).
The REST mount reads it to warn at boot; execute_query_composed refuses from it with
501. One answer for both, so the boot and the request cannot disagree.
Sourcepub async fn execute_query_composed(
&self,
query_match: &QueryMatch,
embeds: &[EmbedSelection],
counts: &[CountSelection],
variables: Option<&Value>,
security_context: Option<&SecurityContext>,
request_budget: Option<&RequestBudget>,
) -> Result<Value>
pub async fn execute_query_composed( &self, query_match: &QueryMatch, embeds: &[EmbedSelection], counts: &[CountSelection], variables: Option<&Value>, security_context: Option<&SecurityContext>, request_budget: Option<&RequestBudget>, ) -> Result<Value>
Execute a pre-resolved query match with related resources composed into it —
the REST ?select= embed.
One statement, not one read per parent row per level: each embedded level is a
correlated subquery joined into the parent’s, and every level is gated as the read
of its own target it replaces — the target query’s authorization and role gates,
its RLS predicate, and field-level RBAC against the target type, all decided before
anything is sent. See runners::query_composed.
request_budget is charged exactly as by
execute_query_direct: the cost of the whole tree
once, before the statement, and the bytes of what it returns.
§Errors
Everything execute_query_direct returns, for the
root and for every embedded level; FraiseQLError::Validation for a relationship
the parent type does not declare; FraiseQLError::Unsupported from a database
adapter that cannot compose a read.
Sourcepub fn request_budget(&self) -> RequestBudget
pub fn request_budget(&self) -> RequestBudget
The allowances one request holds, to be shared by every read that request issues.
A transport that answers one request with several reads — the REST ?select=
embed, which resolves one sub-read per parent row per level — builds one of these
and passes it by reference to each
execute_query_direct. Without it each sub-read
gets allowances of its own, and then [validation] max_response_bytes bounds
each sub-read rather than the response they add up to, and [security.cost_budget] per_request_max scores each sub-read rather than the request they are all part
of — two ceilings named for a request and enforced on something smaller.
Built here rather than in the transport so the ceilings are read from the compiled configuration in the one place that owns them, and a transport cannot supply a budget with ceilings of its own choosing.
Always a budget, never an Option: the two ceilings are declared independently
and either may be absent, which is a distinction
RequestBudget keeps internally rather than
collapsing into “no budget”.
Sourcepub async fn stream_query_direct(
&self,
query_match: QueryMatch,
variables: Option<Value>,
security_context: Option<SecurityContext>,
) -> Result<JsonRowStream>
pub async fn stream_query_direct( &self, query_match: QueryMatch, variables: Option<Value>, security_context: Option<SecurityContext>, ) -> Result<JsonRowStream>
The same read as execute_query_direct,
delivered as a stream of projected rows (#958).
The export representations — NDJSON, CSV, XLSX — consume rows, not a
response envelope, and used to obtain them by re-executing
execute_query_direct with a walking OFFSET. That is O(offset) per page
and gives each page its own snapshot, so a concurrent write can move a row
across a page boundary and the export duplicates or drops it. One statement
has neither problem.
The read resolves through the same authorization, RLS, inject_params and
field-RBAC path as the buffered call — see
QueryRunner::resolve_direct_read.
⚠ On PostgreSQL the returned stream holds a pooled connection until it is dropped. Consume it promptly and drop it when the response ends.
§Errors
Returns FraiseQLError::Authorization if the operation or a selected field is
refused, FraiseQLError::Validation if the query has no SQL source or a
required principal is absent, and FraiseQLError::Database if the read fails
before its first row.
Sourcepub async fn execute_row_read(
&self,
query_match: &QueryMatch,
variables: Option<&Value>,
security_context: Option<&SecurityContext>,
columns: &[ColumnSpec],
) -> Result<RowRead>
pub async fn execute_row_read( &self, query_match: &QueryMatch, variables: Option<&Value>, security_context: Option<&SecurityContext>, columns: &[ColumnSpec], ) -> Result<RowRead>
Execute a row-shaped read — the same read as
execute_query_direct, answered as typed
column values rather than GraphQL-shaped JSON (#1351).
This is the gRPC transport’s read entry. gRPC answers row-shaped results
projected through protobuf ColumnSpecs built from the message descriptor,
and there is no selection set on the wire — so it cannot use the JSON entry
without a round-trip through a shape it chose this transport to avoid. It
used to resolve its own predicate and call
DatabaseAdapter::execute_row_query directly instead, which made it a
second read implementation: the operation Authorizer (#422), the
requires_role gate (#1122), the actor allow-list (#966), the field gate
(#423) and the compiled page-size ceiling (#421) applied to every transport
but that one.
The caller must project with RowRead::columns,
not with the columns it
passed in — see that field for why.
§Errors
Returns FraiseQLError::Authorization when the operation, the role gate, the
actor gate or a selected gated field is refused; FraiseQLError::Validation
when the query has no SQL source or a configured policy has no principal to
evaluate for (fail closed, #784); FraiseQLError::Unsupported for a resolved
shape the row path cannot carry; and FraiseQLError::Database if the read
fails.
Sourcepub async fn stream_row_read(
&self,
query_match: &QueryMatch,
variables: Option<&Value>,
security_context: Option<&SecurityContext>,
columns: &[ColumnSpec],
) -> Result<StreamedRowRead>
pub async fn stream_row_read( &self, query_match: &QueryMatch, variables: Option<&Value>, security_context: Option<&SecurityContext>, columns: &[ColumnSpec], ) -> Result<StreamedRowRead>
The same read as execute_row_read, delivered one
row at a time (#1351).
The gRPC server-streaming arm’s source. Both arms resolve through one function, because the two have already drifted apart once: #1348 found the unary arm fail-closed and the streaming arm fail-open on the same RLS evaluation failure.
⚠ On PostgreSQL the returned stream holds a pooled connection until it is dropped. Consume it promptly and drop it when the response ends.
§Errors
The same as execute_row_read.
Source§impl Executor
impl Executor
Sourcepub fn new_with_relay<A: Writer + RelayDatabaseAdapter + 'static>(
schema: CompiledSchema,
adapter: Arc<A>,
) -> Self
pub fn new_with_relay<A: Writer + RelayDatabaseAdapter + 'static>( schema: CompiledSchema, adapter: Arc<A>, ) -> Self
Create a new executor with relay cursor pagination enabled.
Only callable when A: RelayDatabaseAdapter. The relay capability is
encoded once at construction time as a type-erased Arc<dyn RelayDispatch>,
so there is no per-query overhead beyond an Option::is_some() check.
§Example
// Requires: a live PostgreSQL database with relay support.
// See: tests/integration/ for runnable examples.
let adapter = PostgresAdapter::new(connection_string).await?;
let executor = Executor::new_with_relay(schema, Arc::new(adapter));Sourcepub fn with_config_and_relay<A: Writer + RelayDatabaseAdapter + 'static>(
schema: CompiledSchema,
adapter: Arc<A>,
config: RuntimeConfig,
) -> Self
pub fn with_config_and_relay<A: Writer + RelayDatabaseAdapter + 'static>( schema: CompiledSchema, adapter: Arc<A>, config: RuntimeConfig, ) -> Self
Create a new executor with relay support and custom configuration.
Source§impl Executor
impl Executor
Sourcepub async fn execute(
&self,
query: &str,
variables: Option<&Value>,
) -> Result<Value>
pub async fn execute( &self, query: &str, variables: Option<&Value>, ) -> Result<Value>
Execute a GraphQL query string and return a serialized JSON response.
Applies the configured query timeout if one is set. Handles queries, mutations, introspection, federation, and node lookups.
If RuntimeConfig::query_validation is set, QueryValidator::validate()
runs first (before parsing or SQL dispatch) to enforce size, depth, and
complexity limits. This protects direct fraiseql-core embedders that do
not route through fraiseql-server.
§Errors
FraiseQLError::Validation— query violates configured depth/complexity/alias limits (only whenRuntimeConfig::query_validationisSome).FraiseQLError::Timeout— query exceededRuntimeConfig::query_timeout_ms.- Any error returned by
execute_dispatch.
Sourcepub async fn execute_operation(
&self,
query: &str,
variables: Option<&Value>,
operation_name: Option<&str>,
) -> Result<Value>
pub async fn execute_operation( &self, query: &str, variables: Option<&Value>, operation_name: Option<&str>, ) -> Result<Value>
Execute a document, selecting the operation named by operation_name
(GraphQL § 6.1 GetOperation).
execute is this with None, which requires the
document to define exactly one operation.
§Errors
FraiseQLError::Parse— the document does not parse, names an operation that does not exist, or defines several operations whileoperation_nameisNone.- Any error returned by
execute.
Sourcepub async fn execute_with_scopes(
&self,
query: &str,
variables: Option<&Value>,
user_scopes: &[String],
) -> Result<Value>
pub async fn execute_with_scopes( &self, query: &str, variables: Option<&Value>, user_scopes: &[String], ) -> Result<Value>
Execute a GraphQL query with user context for field-level access control.
This method validates that the user has permission to access all requested
fields before executing the query. If field filtering is enabled in the
RuntimeConfig and the user lacks required scopes, this returns an error.
§Arguments
query- GraphQL query stringvariables- Query variables (optional)user_scopes- User’s scopes from JWT token (pass empty slice if unauthenticated)
§Returns
GraphQL response as JSON string, or error if access denied
§Errors
FraiseQLError::Validation— query validation fails, or the user’s scopes do not include a field required by thefield_filterpolicy.- Propagates errors from query classification and execution.
§Example
// Requires: a live database adapter and authenticated user context.
// See: tests/integration/ for runnable examples.
let query = r#"query { users { id name salary } }"#;
// let user_scopes = user.scopes.clone();
// let result = executor.execute_with_scopes(query, None, &user_scopes).await?;Source§impl Executor
The typed write entries.
impl Executor
The typed write entries.
These used to sit on an impl<A: DatabaseAdapter + SupportsMutations> block, and the
bound was described as the thing keeping a read-only adapter out. It kept out an
adapter without the marker; it could not speak for supports_mutations(), the
runtime gate execute_function_call is keyed on, and these entries skipped that check
on the strength of the bound. An adapter carrying one and not the other was dispatched.
The question is now asked once, at step 0 of the chokepoint, against the write handle
the executor holds. The compile-time refusal did not disappear: it lives on
Executor::new, which is bounded on Writer — implementing its one method is the
capability (ruling AA 6) — and is where the compile_fail pair lives. An adapter that
does not write cannot be built into a write-capable executor.
Sourcepub async fn execute_mutation(
&self,
mutation_name: &str,
variables: Option<&Value>,
selections: WriteSelections<'_>,
) -> Result<Value>
pub async fn execute_mutation( &self, mutation_name: &str, variables: Option<&Value>, selections: WriteSelections<'_>, ) -> Result<Value>
Execute a GraphQL mutation directly, by name.
Unlike execute(), which accepts a raw GraphQL string, this takes the mutation
name and variables directly. Capability is settled by the executor’s write slot:
an executor built by Executor::read_only holds no write handle and refuses here
before any other gate runs.
§Arguments
mutation_name- The GraphQL mutation field name (e.g."createUser")variables- Optional JSON object of GraphQL variable valuesselections- The result selection set (inline fragments intact) used to project the response; pass&[]for no field filtering.
§Returns
A JSON-encoded GraphQL response value on success.
§Errors
Same as execute_mutation_query, minus the adapter
capability check.
Sourcepub async fn execute_mutation_as(
&self,
mutation_name: &str,
variables: Option<&Value>,
security_context: Option<&SecurityContext>,
selections: WriteSelections<'_>,
) -> Result<MutationExecution>
pub async fn execute_mutation_as( &self, mutation_name: &str, variables: Option<&Value>, security_context: Option<&SecurityContext>, selections: WriteSelections<'_>, ) -> Result<MutationExecution>
Execute a mutation with a principal, binding arguments by name from
variables (#1330).
This is execute_mutation plus the caller’s
identity, and it is the entry a non-GraphQL transport should use when it
already holds structured arguments. It converges at
execute_mutation_impl like every other write, so requires_role,
requires_actor, the Authorizer, argument validation, before:mutation
and the change-log write all run.
execute_mutation_with_security is
the same call with the selection set derived from the mutation’s return type
rather than supplied by the caller — the entry a transport uses when it has no
selection set of its own. It used to reach the engine by formatting a GraphQL
document out of the arguments, a round-trip through text that could not
represent every JSON value faithfully; that is fixed (#1331).
§Errors
Same as execute_mutation, plus the refusals the
gates raise for this principal.
Returns the projection and the parsed mutation_response envelope: a
transport whose own wire format is that envelope cannot reconstruct
entity_id or a failure message from the projection alone.
Sourcepub async fn execute_mutation_with_security(
&self,
mutation_name: &str,
arguments: &Value,
security_context: Option<&SecurityContext>,
) -> Result<Value>
pub async fn execute_mutation_with_security( &self, mutation_name: &str, arguments: &Value, security_context: Option<&SecurityContext>, ) -> Result<Value>
Execute a mutation for a transport that holds structured arguments, with an optional principal.
Both arms of the REST write come here — authenticated and anonymous — so both are projected through the same selection set and face the same gates (#1352).
This used to reach the engine by formatting a GraphQL document out of its
arguments: format!("{k}: {v}") renders a serde_json::Value through Display,
which emits JSON, and JSON quotes object keys where GraphQL does not. Any argument
that was — or contained — an object produced a document the parser refused, so an
authenticated REST write carrying a nested body failed outright, and under the
JSONB data-column model a nested object is the ordinary body shape (#1331).
Arguments are bound as values now; nothing round-trips through text.
§Errors
Returns FraiseQLError::Database if the adapter returns an error.
Returns FraiseQLError::Validation if inject params require a missing security context.
Sourcepub async fn execute_mutation_batch(
&self,
mutation_name: &str,
items: &[Value],
security_context: Option<&SecurityContext>,
) -> Result<BulkResult>
pub async fn execute_mutation_batch( &self, mutation_name: &str, items: &[Value], security_context: Option<&SecurityContext>, ) -> Result<BulkResult>
Execute a batch of mutations (for REST bulk insert).
Executes each mutation individually and collects results into a BulkResult.
§Errors
Returns the first error encountered during batch execution.
Sourcepub async fn execute_bulk_by_ids(
&self,
mutation_name: &str,
id_field: &str,
ids: &[Value],
body: Option<&Value>,
security_context: Option<&SecurityContext>,
) -> Result<BulkResult>
pub async fn execute_bulk_by_ids( &self, mutation_name: &str, id_field: &str, ids: &[Value], body: Option<&Value>, security_context: Option<&SecurityContext>, ) -> Result<BulkResult>
Execute a mutation once per identified row — the engine behind a collection-level
PATCH/DELETE.
ids are the primary-key values the caller’s filter selected; each is merged into
the request body under id_field so the mutation function receives the row it is
meant to act on. affected_rows is the number of mutations that actually ran.
This replaces execute_bulk_by_filter, which ran the filter query, discarded
the matched rows, invoked the mutation exactly once with the body and no row
identity, and then reported affected_rows as the filter’s row count — a
fabricated success on a write path (#913). Its _id_field and _max_affected
parameters were both unused.
Row selection and the max_affected cap now live in the caller (the REST bulk
handler), where the filter guard and the HTTP status for “too many rows” belong.
Keeping them there is deliberate: this function can no longer run without a
caller having decided which rows it applies to.
§Errors
Returns whatever the underlying mutation returns; the first failure aborts and propagates, so a partially-applied bulk reports the error rather than a count.
Source§impl Executor
impl Executor
Sourcepub fn plan_subscription(
&self,
document: &ParsedQuery,
variables: Option<&Value>,
principal: Option<&SecurityContext>,
) -> Result<SubscriptionPlan>
pub fn plan_subscription( &self, document: &ParsedQuery, variables: Option<&Value>, principal: Option<&SecurityContext>, ) -> Result<SubscriptionPlan>
Plan a subscription for one subscriber: document is the subscription operation as
the client sent it, variables its variables, principal the subscriber.
§Errors
Validation for a document that selects no subscription or more than one, an unknown
subscription, a field the type does not declare or the subscription does not deliver;
Authorization for the type’s role, a filter on a field the subscriber may not read,
and whatever the read plan refuses.
Sourcepub fn plan_type_stream(
&self,
type_name: &str,
principal: Option<&SecurityContext>,
) -> Result<SubscriptionPlan>
pub fn plan_type_stream( &self, type_name: &str, principal: Option<&SecurityContext>, ) -> Result<SubscriptionPlan>
Plan a stream of type_name’s change events for one reader — the REST
/{resource}/stream (ruling AC 7): a read of the type through the selection a GET
of the resource with no ?select= reads, every field the type declares, object
fields expanded as their type.
§Errors
Validation for a type nesting an object deeper than a whole-object read expands;
Authorization for the type’s role and whatever the read plan refuses — a Reject
field the reader may not read refuses the stream, as it refuses the GET.
Source§impl Executor
impl Executor
Sourcepub async fn execute_with_context(
&self,
query: &str,
variables: Option<&Value>,
ctx: &ExecutionContext,
) -> Result<Value>
pub async fn execute_with_context( &self, query: &str, variables: Option<&Value>, ctx: &ExecutionContext, ) -> Result<Value>
Execute a GraphQL query with cancellation support via ExecutionContext.
This method allows graceful cancellation of long-running queries through a
cancellation token. If the token is cancelled during execution, the query
returns a FraiseQLError::Cancelled error.
§Arguments
query- GraphQL query stringvariables- Query variables (optional)ctx-ExecutionContextwith cancellation token
§Returns
GraphQL response as JSON string, or error if cancelled or execution fails
§Errors
FraiseQLError::Cancelled— the cancellation token was triggered before or during execution.- Propagates any error from the underlying
executecall.
§Example
// Requires: a live database adapter and running tokio runtime.
// See: tests/integration/ for runnable examples.
use fraiseql_core::runtime::ExecutionContext;
use fraiseql_core::error::FraiseQLError;
use std::time::Duration;
let ctx = ExecutionContext::new("user-query-123".to_string());
let cancel_token = ctx.cancellation_token().clone();
// Spawn a task to cancel after 5 seconds
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(5)).await;
cancel_token.cancel();
});
// let result = executor.execute_with_context(query, None, &ctx).await;Sourcepub async fn execute_with_security(
&self,
query: &str,
variables: Option<&Value>,
security_context: &SecurityContext,
) -> Result<Value>
pub async fn execute_with_security( &self, query: &str, variables: Option<&Value>, security_context: &SecurityContext, ) -> Result<Value>
Execute a GraphQL query or mutation with a JWT SecurityContext.
This is the main authenticated entry point for the executor. It routes the incoming request to the appropriate handler based on the query type:
- Regular queries: RLS
WHEREclauses are applied so each user only sees their own rows, as determined by the RLS policy inRuntimeConfig. - Mutations: the security context is forwarded so server-side
injectparameters (e.g.jwt:sub) are resolved from the caller’s JWT claims. - Multi-root queries (e.g.
{ users { id } posts { id } }): each root is dispatched in parallel with the security context applied to every root (H19). - Aggregations, window queries, federation, node lookups: the security context is
forwarded to each handler (RLS /
requires_role/injectgates apply). - Introspection: served from the pre-built response (no per-user data).
If query_timeout_ms is non-zero in the RuntimeConfig, the entire
execution is raced against a Tokio deadline and returns
FraiseQLError::Timeout when the deadline is exceeded.
§Arguments
query- GraphQL query string (e.g."query { posts { id title } }")variables- Optional JSON object of GraphQL variable valuessecurity_context- Authenticated user context extracted from a validated JWT
§Returns
A JSON-encoded GraphQL response string on success, conforming to the GraphQL over HTTP specification.
§Errors
FraiseQLError::Parse— the query string is not valid GraphQLFraiseQLError::Validation— unknown mutation name, missingsql_source, or a mutation requiresinjectparams but the security context is absentFraiseQLError::Database— the underlying adapter returns an errorFraiseQLError::Timeout— execution exceededquery_timeout_ms
§Example
// Requires: a live database adapter and a SecurityContext from authentication.
// See: tests/integration/ for runnable examples.
use fraiseql_core::security::SecurityContext;
// let query = r#"query { posts { id title } }"#;
// Returns a JSON string: {"data":{"posts":[...]}}
// let result = executor.execute_with_security(query, None, &context).await?;Sourcepub async fn execute_operation_with_security(
&self,
query: &str,
variables: Option<&Value>,
security_context: &SecurityContext,
operation_name: Option<&str>,
) -> Result<Value>
pub async fn execute_operation_with_security( &self, query: &str, variables: Option<&Value>, security_context: &SecurityContext, operation_name: Option<&str>, ) -> Result<Value>
Authenticated execution that selects the operation named by
operation_name (GraphQL § 6.1 GetOperation).
execute_with_security is this with
None, which requires the document to define exactly one operation.
Every HTTP request carries an operationName field, so the server path
uses this entry point.
§Errors
FraiseQLError::Parse— the document does not parse, names an operation that does not exist, or defines several operations whileoperation_nameisNone.- Any error returned by
execute_with_security.
Sourcepub fn check_field_access(
&self,
type_name: &str,
field_name: &str,
user_scopes: &[String],
) -> Result<(), FieldAccessError>
pub fn check_field_access( &self, type_name: &str, field_name: &str, user_scopes: &[String], ) -> Result<(), FieldAccessError>
Check if a specific field can be accessed with given scopes.
This is a convenience method for checking field access without executing a query.
§Arguments
type_name- The GraphQL type namefield_name- The field nameuser_scopes- User’s scopes from JWT token
§Returns
Ok(()) if access is allowed, Err(FieldAccessError) if denied
§Errors
Returns FieldAccessError::AccessDenied if the user’s scopes do not include the
required scope for the field.
Sourcepub async fn execute_json(
&self,
query: &str,
variables: Option<&Value>,
) -> Result<Value>
👎Deprecated since 2.2.0: use execute() directly — it now returns Value
pub async fn execute_json( &self, query: &str, variables: Option<&Value>, ) -> Result<Value>
use execute() directly — it now returns Value
Execute a query and return parsed JSON.
This method is now equivalent to execute() since execute() already
returns serde_json::Value.
§Errors
Returns any error from execute().
Source§impl Executor
impl Executor
Sourcepub async fn explain(
&self,
query_name: &str,
variables: Option<&Value>,
limit: Option<u32>,
offset: Option<u32>,
) -> Result<ExplainResult>
pub async fn explain( &self, query_name: &str, variables: Option<&Value>, limit: Option<u32>, offset: Option<u32>, ) -> Result<ExplainResult>
Run EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) for a named query.
Looks up query_name in the compiled schema, builds a parameterized
WHERE clause from variables, and delegates to
DatabaseAdapter::explain_where_query.
The result includes the
generated SQL and the raw PostgreSQL EXPLAIN output.
§Arguments
query_name- Name of a regular query in the schema (e.g.,"users")variables- JSON object whose keys map to equality WHERE conditionslimit- Optional LIMIT to pass to the queryoffset- Optional OFFSET to pass to the query
§Errors
FraiseQLError::Validation— unknown query name or mutation givenFraiseQLError::Unsupported— database adapter does not support EXPLAIN ANALYZEFraiseQLError::Database— EXPLAIN execution failed
Source§impl Executor
impl Executor
Sourcepub async fn execute_parallel(
&self,
parsed: &ParsedQuery,
variables: Option<&Value>,
security_context: Option<&SecurityContext>,
) -> Result<PipelineResult>
pub async fn execute_parallel( &self, parsed: &ParsedQuery, variables: Option<&Value>, security_context: Option<&SecurityContext>, ) -> Result<PipelineResult>
Execute all root fields of a multi-root query concurrently.
Each root field is dispatched as an independent single-root query.
Results are awaited with futures::future::try_join_all and merged
into a PipelineResult.
§Errors
Returns the first error encountered across all concurrent sub-queries.
Source§impl Executor
impl Executor
Sourcepub fn plan_query(
&self,
query: &str,
variables: Option<&Value>,
) -> Result<ExplainPlan>
pub fn plan_query( &self, query: &str, variables: Option<&Value>, ) -> Result<ExplainPlan>
Generate an explain plan for a query without executing it.
Returns the SQL that would be generated, parameters, cost estimate, and views that would be accessed.
§Errors
Returns error if the query cannot be parsed or matched against the schema.