Skip to main content

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