fraiseql_core/runtime/mod.rs
1//! Runtime query executor - executes compiled queries.
2//!
3//! # Architecture
4//!
5//! The runtime loads a `CompiledSchema` and executes incoming GraphQL queries by:
6//! 1. Parsing the GraphQL query
7//! 2. Matching it to a compiled query template
8//! 3. Binding variables
9//! 4. Executing the pre-compiled SQL
10//! 5. Projecting JSONB results to GraphQL response
11//!
12//! # Key Concepts
13//!
14//! - **Zero runtime compilation**: All SQL is pre-compiled
15//! - **Pattern matching**: Match incoming query structure to templates
16//! - **Variable binding**: Safe parameter substitution
17//! - **Result projection**: JSONB → GraphQL JSON transformation
18//!
19//! # Example
20//!
21//! ```no_run
22//! // Requires: a compiled schema file and a live PostgreSQL database.
23//! // See: tests/integration/ for runnable examples.
24//! use fraiseql_core::runtime::Executor;
25//! use fraiseql_core::schema::CompiledSchema;
26//! use fraiseql_core::db::postgres::PostgresAdapter;
27//! use std::sync::Arc;
28//!
29//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
30//! # let schema_json = r#"{"types":[],"queries":[]}"#;
31//! // Load compiled schema
32//! let schema = CompiledSchema::from_json(schema_json)?;
33//!
34//! // Create executor with a concrete adapter implementation
35//! let adapter = Arc::new(PostgresAdapter::new("postgresql://localhost/mydb").await?);
36//! let executor = Executor::new(schema, adapter);
37//!
38//! // Execute GraphQL query
39//! let query = r#"query { users { id name } }"#;
40//! let result = executor.execute(query, None).await?;
41//!
42//! println!("{}", result);
43//! # Ok(())
44//! # }
45//! ```
46
47mod aggregate_parser;
48mod aggregate_projector;
49pub mod aggregation;
50pub mod cascade;
51mod executor;
52pub mod executor_adapter;
53mod explain;
54pub mod field_filter;
55pub mod input_validator;
56pub mod jsonb_strategy;
57mod matcher;
58pub mod mutation_result;
59pub(crate) mod native_columns;
60mod planner;
61mod projection;
62pub mod query_tracing;
63pub mod relay;
64pub mod sql_logger;
65pub mod subscription;
66pub mod tenant_enforcer;
67pub mod window;
68mod window_parser;
69mod window_projector;
70
71use std::sync::Arc;
72
73pub use aggregate_parser::AggregateQueryParser;
74pub use aggregate_projector::AggregationProjector;
75pub use aggregation::{AggregationSqlGenerator, ParameterizedAggregationSql};
76pub use executor::{
77 Executor,
78 pipeline::{extract_root_field_names, is_multi_root, multi_root_queries_total},
79};
80pub use executor_adapter::ExecutorAdapter;
81pub use explain::{ExplainPlan, ExplainResult};
82pub use field_filter::{FieldAccessResult, can_access_field, classify_field_access, filter_fields};
83pub use jsonb_strategy::{JsonbOptimizationOptions, JsonbStrategy};
84pub use matcher::{QueryMatch, QueryMatcher, suggest_similar};
85pub use planner::{ExecutionPlan, QueryPlanner};
86pub use projection::{
87 FieldMapping, ProjectionMapper, ResultProjector, build_field_mappings_from_type,
88};
89pub use query_tracing::{
90 QueryExecutionTrace, QueryPhaseSpan, QueryTraceBuilder, create_phase_span, create_query_span,
91};
92pub use sql_logger::{SqlOperation, SqlQueryLog, SqlQueryLogBuilder, create_sql_span};
93pub use subscription::{
94 ActiveSubscription, DeliveryResult, KafkaAdapter, KafkaConfig, KafkaMessage, SubscriptionError,
95 SubscriptionEvent, SubscriptionId, SubscriptionManager, SubscriptionOperation,
96 SubscriptionPayload, TransportAdapter, TransportManager, WebhookAdapter, WebhookConfig,
97 WebhookPayload, extract_rls_conditions, protocol,
98};
99pub use tenant_enforcer::TenantEnforcer;
100
101/// Result of a bulk REST operation (collection-level PATCH/DELETE).
102#[derive(Debug, Clone)]
103pub struct BulkResult {
104 /// Number of rows affected.
105 pub affected_rows: u64,
106 /// Entities returned when `Prefer: return=representation` is set.
107 pub entities: Option<Vec<serde_json::Value>>,
108}
109pub use window::{WindowSql, WindowSqlGenerator};
110pub use window_parser::WindowQueryParser;
111pub use window_projector::WindowProjector;
112
113use crate::security::{FieldFilter, FieldFilterConfig, QueryValidatorConfig, RLSPolicy};
114
115/// Runtime configuration for the FraiseQL query executor.
116///
117/// Controls safety limits, security policies, and performance tuning. All settings
118/// have production-safe defaults and can be overridden via the builder-style methods.
119///
120/// # Defaults
121///
122/// | Field | Default | Notes |
123/// |-------|---------|-------|
124/// | `cache_query_plans` | `true` | Caches parsed query plans for repeated queries |
125/// | `max_query_depth` | `10` | Prevents stack overflow on recursive GraphQL |
126/// | `max_query_complexity` | `1000` | Rough cost model; tune per workload |
127/// | `enable_tracing` | `false` | Emit `OpenTelemetry` spans for each query |
128/// | `query_timeout_ms` | `30 000` | Hard limit; 0 disables the timeout |
129/// | `field_filter` | `None` | No field-level access control |
130/// | `rls_policy` | `None` | No row-level security |
131///
132/// # Example
133///
134/// ```
135/// use fraiseql_core::runtime::RuntimeConfig;
136/// use fraiseql_core::security::FieldFilterConfig;
137///
138/// let config = RuntimeConfig {
139/// max_query_depth: 5,
140/// max_query_complexity: 500,
141/// enable_tracing: true,
142/// query_timeout_ms: 5_000,
143/// ..RuntimeConfig::default()
144/// }
145/// .with_field_filter(
146/// FieldFilterConfig::new()
147/// .protect_field("User", "salary")
148/// .protect_field("User", "ssn"),
149/// );
150/// ```
151pub struct RuntimeConfig {
152 /// Enable query plan caching.
153 pub cache_query_plans: bool,
154
155 /// Maximum query depth (prevents deeply nested queries).
156 pub max_query_depth: usize,
157
158 /// Maximum query complexity score.
159 pub max_query_complexity: usize,
160
161 /// Enable performance tracing.
162 pub enable_tracing: bool,
163
164 /// Optional field filter for access control.
165 /// When set, validates that users have required scopes to access fields.
166 pub field_filter: Option<FieldFilter>,
167
168 /// Optional row-level security (RLS) policy.
169 /// When set, evaluates access rules based on `SecurityContext` to determine
170 /// what rows a user can access (e.g., tenant isolation, owner-based access).
171 pub rls_policy: Option<Arc<dyn RLSPolicy>>,
172
173 /// Query timeout in milliseconds (0 = no timeout).
174 pub query_timeout_ms: u64,
175
176 /// JSONB field optimization strategy options
177 pub jsonb_optimization: JsonbOptimizationOptions,
178
179 /// Optional query validation config.
180 ///
181 /// When `Some`, `QueryValidator::validate()` runs at the start of every
182 /// `Executor::execute()` call, before any parsing or SQL dispatch.
183 /// This provides `DoS` protection for direct `fraiseql-core` embedders that
184 /// do not route through `fraiseql-server` (which already runs `RequestValidator`
185 /// at the HTTP layer). Enforces: query size, depth, complexity, and alias count
186 /// (alias amplification protection).
187 ///
188 /// Set `None` to disable (default) — useful when the caller applies
189 /// validation at a higher layer, or when `fraiseql-server` is in use.
190 pub query_validation: Option<QueryValidatorConfig>,
191}
192
193impl std::fmt::Debug for RuntimeConfig {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 f.debug_struct("RuntimeConfig")
196 .field("cache_query_plans", &self.cache_query_plans)
197 .field("max_query_depth", &self.max_query_depth)
198 .field("max_query_complexity", &self.max_query_complexity)
199 .field("enable_tracing", &self.enable_tracing)
200 .field("field_filter", &self.field_filter.is_some())
201 .field("rls_policy", &self.rls_policy.is_some())
202 .field("query_timeout_ms", &self.query_timeout_ms)
203 .field("jsonb_optimization", &self.jsonb_optimization)
204 .field("query_validation", &self.query_validation)
205 .finish()
206 }
207}
208
209impl Default for RuntimeConfig {
210 fn default() -> Self {
211 Self {
212 cache_query_plans: true,
213 max_query_depth: 10,
214 max_query_complexity: 1000,
215 enable_tracing: false,
216 field_filter: None,
217 rls_policy: None,
218 query_timeout_ms: 30_000, // 30 second default timeout
219 jsonb_optimization: JsonbOptimizationOptions::default(),
220 query_validation: None,
221 }
222 }
223}
224
225impl RuntimeConfig {
226 /// Create a new runtime config with a field filter.
227 ///
228 /// # Example
229 ///
230 /// ```
231 /// use fraiseql_core::runtime::RuntimeConfig;
232 /// use fraiseql_core::security::FieldFilterConfig;
233 ///
234 /// let config = RuntimeConfig::default()
235 /// .with_field_filter(
236 /// FieldFilterConfig::new()
237 /// .protect_field("User", "salary")
238 /// .protect_field("User", "ssn")
239 /// );
240 /// ```
241 #[must_use]
242 pub fn with_field_filter(mut self, config: FieldFilterConfig) -> Self {
243 self.field_filter = Some(FieldFilter::new(config));
244 self
245 }
246
247 /// Configure row-level security (RLS) policy for access control.
248 ///
249 /// When set, the executor will evaluate the RLS policy before executing queries,
250 /// applying WHERE clause filters based on the user's `SecurityContext`.
251 ///
252 /// # Example
253 ///
254 /// ```rust
255 /// use fraiseql_core::runtime::RuntimeConfig;
256 /// use fraiseql_core::security::DefaultRLSPolicy;
257 /// use std::sync::Arc;
258 ///
259 /// let config = RuntimeConfig::default()
260 /// .with_rls_policy(Arc::new(DefaultRLSPolicy::new()));
261 /// ```
262 #[must_use]
263 pub fn with_rls_policy(mut self, policy: Arc<dyn RLSPolicy>) -> Self {
264 self.rls_policy = Some(policy);
265 self
266 }
267}
268
269/// Execution context for query cancellation support.
270///
271/// This struct provides a mechanism for gracefully cancelling long-running queries
272/// via cancellation tokens, enabling proper cleanup and error reporting when:
273/// - A client connection closes
274/// - A user explicitly cancels a query
275/// - A system shutdown is initiated
276///
277/// # Example
278///
279/// ```no_run
280/// // Requires: a running tokio runtime and an Executor with a live database adapter.
281/// // See: tests/integration/ for runnable examples.
282/// use fraiseql_core::runtime::ExecutionContext;
283/// use std::time::Duration;
284///
285/// let ctx = ExecutionContext::new("query-123".to_string());
286///
287/// // Spawn a task that cancels after 5 seconds
288/// let cancel_token = ctx.cancellation_token().clone();
289/// tokio::spawn(async move {
290/// tokio::time::sleep(Duration::from_secs(5)).await;
291/// cancel_token.cancel();
292/// });
293///
294/// // Execute query with cancellation support
295/// // let result = executor.execute_with_context(query, None, &ctx).await;
296/// ```
297#[derive(Debug, Clone)]
298pub struct ExecutionContext {
299 /// Unique identifier for tracking the query execution
300 query_id: String,
301
302 /// Cancellation token for gracefully stopping the query
303 /// When cancelled, ongoing query execution should stop and return a Cancelled error
304 token: tokio_util::sync::CancellationToken,
305}
306
307impl ExecutionContext {
308 /// Create a new execution context with a cancellation token.
309 ///
310 /// # Arguments
311 ///
312 /// * `query_id` - Unique identifier for this query execution
313 ///
314 /// # Example
315 ///
316 /// ```rust
317 /// # use fraiseql_core::runtime::ExecutionContext;
318 /// let ctx = ExecutionContext::new("user-query-001".to_string());
319 /// assert_eq!(ctx.query_id(), "user-query-001");
320 /// ```
321 #[must_use]
322 pub fn new(query_id: String) -> Self {
323 Self {
324 query_id,
325 token: tokio_util::sync::CancellationToken::new(),
326 }
327 }
328
329 /// Get the query ID.
330 #[must_use]
331 pub fn query_id(&self) -> &str {
332 &self.query_id
333 }
334
335 /// Get a reference to the cancellation token.
336 ///
337 /// The returned token can be used to:
338 /// - Clone and pass to background tasks
339 /// - Check if cancellation was requested
340 /// - Propagate cancellation through the call stack
341 #[must_use]
342 pub const fn cancellation_token(&self) -> &tokio_util::sync::CancellationToken {
343 &self.token
344 }
345
346 /// Check if cancellation has been requested.
347 #[must_use]
348 pub fn is_cancelled(&self) -> bool {
349 self.token.is_cancelled()
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 #[test]
358 fn test_default_config() {
359 let config = RuntimeConfig::default();
360 assert!(config.cache_query_plans);
361 assert_eq!(config.max_query_depth, 10);
362 assert_eq!(config.max_query_complexity, 1000);
363 assert!(!config.enable_tracing);
364 }
365}