Skip to main content

fraiseql_core/runtime/executor/
core.rs

1//! `Executor<A>` struct definition, constructors, and basic accessors.
2
3use std::{collections::HashMap, sync::Arc};
4
5use super::relay::{RelayDispatch, RelayDispatchImpl};
6use crate::{
7    db::{RelayDatabaseAdapter, traits::DatabaseAdapter, types::PoolMetrics},
8    runtime::{QueryMatcher, QueryPlanner, RuntimeConfig},
9    schema::{CompiledSchema, IntrospectionResponses},
10};
11
12/// Query executor - executes compiled GraphQL queries.
13///
14/// This is the main entry point for runtime query execution.
15/// It coordinates matching, planning, execution, and projection.
16///
17/// # Type Parameters
18///
19/// * `A` - The database adapter type (implements `DatabaseAdapter` trait)
20///
21/// # Ownership and Lifetimes
22///
23/// The executor holds owned references to schema and runtime data, with no borrowed pointers:
24/// - `schema`: Owned `CompiledSchema` (immutable after construction)
25/// - `adapter`: Shared via `Arc<A>` to allow multiple executors/tasks to use the same connection
26///   pool
27/// - `introspection`: Owned cached GraphQL schema responses
28/// - `config`: Owned runtime configuration
29///
30/// **No explicit lifetimes required** - all data is either owned or wrapped in `Arc`,
31/// so the executor can be stored in long-lived structures without lifetime annotations or
32/// borrow-checker issues.
33///
34/// # Concurrency
35///
36/// `Executor<A>` is `Send + Sync` when `A` is `Send + Sync`. It can be safely shared across
37/// threads and tasks without cloning:
38/// ```no_run
39/// // Requires: a live database adapter.
40/// // See: tests/integration/ for runnable examples.
41/// # use std::sync::Arc;
42/// // let executor = Arc::new(Executor::new(schema, adapter));
43/// // Can be cloned into multiple tasks
44/// // let exec_clone = executor.clone();
45/// // tokio::spawn(async move {
46/// //     let result = exec_clone.execute(query, vars).await;
47/// // });
48/// ```
49///
50/// # Query Timeout
51///
52/// Queries are protected by the `query_timeout_ms` configuration in `RuntimeConfig` (default: 30s).
53/// When a query exceeds this timeout, it returns `FraiseQLError::Timeout` without panicking.
54/// Set `query_timeout_ms` to 0 to disable timeout enforcement.
55pub struct Executor<A: DatabaseAdapter> {
56    /// Compiled schema with optimized SQL templates
57    pub(super) schema: CompiledSchema,
58
59    /// Shared database adapter for query execution
60    /// Wrapped in Arc to allow multiple executors to use the same connection pool
61    pub(super) adapter: Arc<A>,
62
63    /// Type-erased relay capability slot.
64    ///
65    /// `Some` when the executor was constructed via `new_with_relay` (requires
66    /// `A: RelayDatabaseAdapter`).  `None` causes relay queries to return a
67    /// `FraiseQLError::Validation` — no `unreachable!()`, no capability flag.
68    pub(super) relay: Option<Arc<dyn RelayDispatch>>,
69
70    /// Query matching engine (stateless)
71    pub(super) matcher: QueryMatcher,
72
73    /// Query execution planner (stateless)
74    pub(super) planner: QueryPlanner,
75
76    /// Runtime configuration (timeouts, complexity limits, etc.)
77    pub(super) config: RuntimeConfig,
78
79    /// Pre-built introspection responses cached for `__schema` and `__type` queries
80    /// Avoids recomputing schema introspection on every request
81    pub(super) introspection: IntrospectionResponses,
82
83    /// O(1) lookup index for Relay `node(id)` queries: maps `return_type → sql_source`.
84    ///
85    /// Built once at `Executor::with_config()` from the compiled schema, so every
86    /// `execute_node_query()` call is a single `HashMap::get()` rather than an O(N)
87    /// linear scan over `schema.queries`.
88    pub(super) node_type_index: HashMap<String, Arc<str>>,
89}
90
91impl<A: DatabaseAdapter> Executor<A> {
92    /// Create new executor.
93    ///
94    /// # Arguments
95    ///
96    /// * `schema` - Compiled schema
97    /// * `adapter` - Database adapter
98    ///
99    /// # Example
100    ///
101    /// ```no_run
102    /// // Requires: a live PostgreSQL database.
103    /// // See: tests/integration/ for runnable examples.
104    /// # use fraiseql_core::schema::CompiledSchema;
105    /// # use fraiseql_core::db::postgres::PostgresAdapter;
106    /// # use fraiseql_core::runtime::Executor;
107    /// # use std::sync::Arc;
108    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
109    /// # let schema_json = r#"{"types":[],"queries":[]}"#;
110    /// # let connection_string = "postgresql://localhost/mydb";
111    /// let schema = CompiledSchema::from_json(schema_json)?;
112    /// let adapter = PostgresAdapter::new(connection_string).await?;
113    /// let executor = Executor::new(schema, Arc::new(adapter));
114    /// # Ok(()) }
115    /// ```
116    #[must_use]
117    pub fn new(schema: CompiledSchema, adapter: Arc<A>) -> Self {
118        Self::with_config(schema, adapter, RuntimeConfig::default())
119    }
120
121    /// Create new executor with custom configuration.
122    ///
123    /// # Arguments
124    ///
125    /// * `schema` - Compiled schema
126    /// * `adapter` - Database adapter
127    /// * `config` - Runtime configuration
128    #[must_use]
129    pub fn with_config(schema: CompiledSchema, adapter: Arc<A>, config: RuntimeConfig) -> Self {
130        let matcher = QueryMatcher::new(schema.clone());
131        let planner = QueryPlanner::new(config.cache_query_plans);
132        // Build introspection responses at startup (zero-cost at runtime)
133        let introspection = IntrospectionResponses::build(&schema);
134
135        // Build O(1) node-type index: return_type → sql_source.
136        // The first query with a matching return_type and a non-None sql_source wins
137        // (consistent with the previous linear-scan behaviour).
138        let mut node_type_index: HashMap<String, Arc<str>> = HashMap::new();
139        for q in &schema.queries {
140            if let Some(src) = q.sql_source.as_deref() {
141                node_type_index.entry(q.return_type.clone()).or_insert_with(|| Arc::from(src));
142            }
143        }
144
145        Self {
146            schema,
147            adapter,
148            relay: None,
149            matcher,
150            planner,
151            config,
152            introspection,
153            node_type_index,
154        }
155    }
156
157    /// Return current connection pool metrics from the underlying database adapter.
158    ///
159    /// Values are sampled live on each call — not cached — so callers (e.g., the
160    /// `/metrics` endpoint) always observe up-to-date pool health.
161    pub fn pool_metrics(&self) -> PoolMetrics {
162        self.adapter.pool_metrics()
163    }
164
165    /// Get the compiled schema.
166    #[must_use]
167    pub const fn schema(&self) -> &CompiledSchema {
168        &self.schema
169    }
170
171    /// Get runtime configuration.
172    #[must_use]
173    pub const fn config(&self) -> &RuntimeConfig {
174        &self.config
175    }
176
177    /// Get database adapter reference.
178    #[must_use]
179    pub const fn adapter(&self) -> &Arc<A> {
180        &self.adapter
181    }
182}
183
184impl<A: DatabaseAdapter + RelayDatabaseAdapter + 'static> Executor<A> {
185    /// Create a new executor with relay cursor pagination enabled.
186    ///
187    /// Only callable when `A: RelayDatabaseAdapter`.  The relay capability is
188    /// encoded once at construction time as a type-erased `Arc<dyn RelayDispatch>`,
189    /// so there is no per-query overhead beyond an `Option::is_some()` check.
190    ///
191    /// # Example
192    ///
193    /// ```no_run
194    /// // Requires: a live PostgreSQL database with relay support.
195    /// // See: tests/integration/ for runnable examples.
196    /// # use fraiseql_core::schema::CompiledSchema;
197    /// # use fraiseql_core::db::postgres::PostgresAdapter;
198    /// # use fraiseql_core::runtime::Executor;
199    /// # use std::sync::Arc;
200    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
201    /// # let connection_string = "postgresql://localhost/mydb";
202    /// # let schema: CompiledSchema = panic!("example");
203    /// let adapter = PostgresAdapter::new(connection_string).await?;
204    /// let executor = Executor::new_with_relay(schema, Arc::new(adapter));
205    /// # Ok(()) }
206    /// ```
207    #[must_use]
208    pub fn new_with_relay(schema: CompiledSchema, adapter: Arc<A>) -> Self {
209        Self::with_config_and_relay(schema, adapter, RuntimeConfig::default())
210    }
211
212    /// Create a new executor with relay support and custom configuration.
213    #[must_use]
214    pub fn with_config_and_relay(
215        schema: CompiledSchema,
216        adapter: Arc<A>,
217        config: RuntimeConfig,
218    ) -> Self {
219        let relay: Arc<dyn RelayDispatch> = Arc::new(RelayDispatchImpl(adapter.clone()));
220        let matcher = QueryMatcher::new(schema.clone());
221        let planner = QueryPlanner::new(config.cache_query_plans);
222        let introspection = IntrospectionResponses::build(&schema);
223
224        let mut node_type_index: HashMap<String, Arc<str>> = HashMap::new();
225        for q in &schema.queries {
226            if let Some(src) = q.sql_source.as_deref() {
227                node_type_index.entry(q.return_type.clone()).or_insert_with(|| Arc::from(src));
228            }
229        }
230
231        Self {
232            schema,
233            adapter,
234            relay: Some(relay),
235            matcher,
236            planner,
237            config,
238            introspection,
239            node_type_index,
240        }
241    }
242}