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::{FieldMapping, ProjectionMapper, ResultProjector, project_entity};
88pub use query_tracing::{
89 QueryExecutionTrace, QueryPhaseSpan, QueryTraceBuilder, create_phase_span, create_query_span,
90};
91pub use sql_logger::{SqlOperation, SqlQueryLog, SqlQueryLogBuilder, create_sql_span};
92pub use subscription::{
93 ActiveSubscription, DeliveryResult, KafkaAdapter, KafkaConfig, KafkaMessage, SubscriptionError,
94 SubscriptionEvent, SubscriptionId, SubscriptionManager, SubscriptionOperation,
95 SubscriptionPayload, TransportAdapter, TransportManager, WebhookAdapter, WebhookConfig,
96 WebhookPayload, extract_rls_conditions, protocol,
97};
98pub use tenant_enforcer::TenantEnforcer;
99
100/// Result of a bulk REST operation (collection-level PATCH/DELETE).
101#[derive(Debug, Clone)]
102pub struct BulkResult {
103 /// Number of rows affected.
104 pub affected_rows: u64,
105 /// Entities returned when `Prefer: return=representation` is set.
106 pub entities: Option<Vec<serde_json::Value>>,
107}
108pub use window::{WindowSql, WindowSqlGenerator};
109pub use window_parser::WindowQueryParser;
110pub use window_projector::WindowProjector;
111
112use crate::security::{
113 Authorizer, FieldAuthorizer, FieldFilter, FieldFilterConfig, QueryValidatorConfig, RLSPolicy,
114};
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/// | `authorizer` | `None` | No operation-level authorization |
133///
134/// # Example
135///
136/// ```
137/// use fraiseql_core::runtime::RuntimeConfig;
138/// use fraiseql_core::security::FieldFilterConfig;
139///
140/// let config = RuntimeConfig {
141/// max_query_depth: 5,
142/// max_query_complexity: 500,
143/// enable_tracing: true,
144/// query_timeout_ms: 5_000,
145/// ..RuntimeConfig::default()
146/// }
147/// .with_field_filter(
148/// FieldFilterConfig::new()
149/// .protect_field("User", "salary")
150/// .protect_field("User", "ssn"),
151/// );
152/// ```
153pub struct RuntimeConfig {
154 /// Enable query plan caching.
155 pub cache_query_plans: bool,
156
157 /// Maximum query depth (prevents deeply nested queries).
158 pub max_query_depth: usize,
159
160 /// Maximum query complexity score.
161 pub max_query_complexity: usize,
162
163 /// Maximum number of rows a top-level `first`/`last`/`limit` argument may
164 /// request, guarding against unbounded-pagination denial of service (#421):
165 /// the top-level row count is the one knob that sizes the database result set
166 /// and the serialized response. A request exceeding this is rejected with a
167 /// [`crate::FraiseQLError::Validation`]. `None` disables the ceiling. Default
168 /// `Some(1000)`.
169 pub max_page_size: Option<u32>,
170
171 /// Enable performance tracing.
172 pub enable_tracing: bool,
173
174 /// Optional field filter for access control.
175 /// When set, validates that users have required scopes to access fields.
176 pub field_filter: Option<FieldFilter>,
177
178 /// Optional row-level security (RLS) policy.
179 /// When set, evaluates access rules based on `SecurityContext` to determine
180 /// what rows a user can access (e.g., tenant isolation, owner-based access).
181 pub rls_policy: Option<Arc<dyn RLSPolicy>>,
182
183 /// Optional dynamic field-level authorizer.
184 ///
185 /// When set, fields marked policy-gated in the compiled schema
186 /// ([`FieldDefinition::authorize`](crate::schema::FieldDefinition)) are passed to
187 /// this authorizer per row, which returns an allow/deny decision based on the
188 /// principal, the parent row, and the field arguments. Composes as a logical AND
189 /// with the static `requires_scope` gate and is fail-closed (any error denies).
190 /// See [`FieldAuthorizer`].
191 pub field_authorizer: Option<Arc<dyn FieldAuthorizer>>,
192
193 /// Optional dynamic operation-level authorizer.
194 ///
195 /// When set, every operation (query, mutation, subscription) is passed to this
196 /// authorizer before dispatch, which returns an allow/deny decision based on the
197 /// principal (or `None` when anonymous), the operation kind and name, and the
198 /// request input. Composes as a logical AND with the static `requires_role` gate
199 /// and is fail-closed (any error or raise denies with HTTP 403 / `FORBIDDEN`).
200 /// See [`Authorizer`].
201 pub authorizer: Option<Arc<dyn Authorizer>>,
202
203 /// Query timeout in milliseconds (0 = no timeout).
204 pub query_timeout_ms: u64,
205
206 /// JSONB field optimization strategy options
207 pub jsonb_optimization: JsonbOptimizationOptions,
208
209 /// Optional query validation config.
210 ///
211 /// When `Some`, `QueryValidator::validate()` runs at the start of every
212 /// `Executor::execute()` call, before any parsing or SQL dispatch.
213 /// This provides `DoS` protection for direct `fraiseql-core` embedders that
214 /// do not route through `fraiseql-server` (which already runs `RequestValidator`
215 /// at the HTTP layer). Enforces: query size, depth, complexity, and alias count
216 /// (alias amplification protection).
217 ///
218 /// Set `None` to disable (default) — useful when the caller applies
219 /// validation at a higher layer, or when `fraiseql-server` is in use.
220 pub query_validation: Option<QueryValidatorConfig>,
221
222 /// Emit structured `tracing` events for every successfully-executed mutation.
223 ///
224 /// When `true`, a `tracing::info!` event with target `"fraiseql::mutation_audit"` is
225 /// emitted at the end of every successful `execute_mutation_query_with_security()` call.
226 /// The event carries fields: `mutation_name`, `entity_type`, `operation`, `tenant_id`.
227 ///
228 /// **Zero-cost when disabled**: the guard `if !self.config.audit_mutations { return }`
229 /// short-circuits before any string formatting or allocation occurs.
230 ///
231 /// Set to `true` when `audit_logging_enabled = true` in the compiled schema's
232 /// `[security.enterprise]` section (threaded through `Server::new()` at startup).
233 pub audit_mutations: bool,
234}
235
236impl std::fmt::Debug for RuntimeConfig {
237 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238 f.debug_struct("RuntimeConfig")
239 .field("cache_query_plans", &self.cache_query_plans)
240 .field("max_query_depth", &self.max_query_depth)
241 .field("max_query_complexity", &self.max_query_complexity)
242 .field("max_page_size", &self.max_page_size)
243 .field("enable_tracing", &self.enable_tracing)
244 .field("field_filter", &self.field_filter.is_some())
245 .field("rls_policy", &self.rls_policy.is_some())
246 .field("field_authorizer", &self.field_authorizer.is_some())
247 .field("authorizer", &self.authorizer.is_some())
248 .field("query_timeout_ms", &self.query_timeout_ms)
249 .field("jsonb_optimization", &self.jsonb_optimization)
250 .field("query_validation", &self.query_validation)
251 .field("audit_mutations", &self.audit_mutations)
252 .finish()
253 }
254}
255
256impl Default for RuntimeConfig {
257 fn default() -> Self {
258 Self {
259 cache_query_plans: true,
260 max_query_depth: 10,
261 max_query_complexity: 1000,
262 max_page_size: Some(1000),
263 enable_tracing: false,
264 field_filter: None,
265 rls_policy: None,
266 field_authorizer: None,
267 authorizer: None,
268 query_timeout_ms: 30_000, // 30 second default timeout
269 jsonb_optimization: JsonbOptimizationOptions::default(),
270 query_validation: None,
271 audit_mutations: false,
272 }
273 }
274}
275
276impl RuntimeConfig {
277 /// Create a new runtime config with a field filter.
278 ///
279 /// # Example
280 ///
281 /// ```
282 /// use fraiseql_core::runtime::RuntimeConfig;
283 /// use fraiseql_core::security::FieldFilterConfig;
284 ///
285 /// let config = RuntimeConfig::default()
286 /// .with_field_filter(
287 /// FieldFilterConfig::new()
288 /// .protect_field("User", "salary")
289 /// .protect_field("User", "ssn")
290 /// );
291 /// ```
292 #[must_use = "builder method returns modified builder"]
293 pub fn with_field_filter(mut self, config: FieldFilterConfig) -> Self {
294 self.field_filter = Some(FieldFilter::new(config));
295 self
296 }
297
298 /// Configure row-level security (RLS) policy for access control.
299 ///
300 /// When set, the executor will evaluate the RLS policy before executing queries,
301 /// applying WHERE clause filters based on the user's `SecurityContext`.
302 ///
303 /// # Example
304 ///
305 /// ```rust
306 /// use fraiseql_core::runtime::RuntimeConfig;
307 /// use fraiseql_core::security::DefaultRLSPolicy;
308 /// use std::sync::Arc;
309 ///
310 /// let config = RuntimeConfig::default()
311 /// .with_rls_policy(Arc::new(DefaultRLSPolicy::new()));
312 /// ```
313 #[must_use = "builder method returns modified builder"]
314 pub fn with_rls_policy(mut self, policy: Arc<dyn RLSPolicy>) -> Self {
315 self.rls_policy = Some(policy);
316 self
317 }
318
319 /// Configure a dynamic field-level authorizer.
320 ///
321 /// When set, fields marked policy-gated in the compiled schema
322 /// ([`FieldDefinition::authorize`](crate::schema::FieldDefinition)) are evaluated
323 /// per row by this authorizer. The decision composes as a logical AND with the
324 /// static `requires_scope` gate and is fail-closed (any error or raise denies
325 /// with HTTP 403 / `FORBIDDEN`). Parallel to [`with_rls_policy`](Self::with_rls_policy).
326 ///
327 /// # Example
328 ///
329 /// ```rust
330 /// use fraiseql_core::runtime::RuntimeConfig;
331 /// use fraiseql_core::security::{
332 /// FieldAuthorizer, FieldAuthzRequest, FieldAuthzDecision,
333 /// };
334 /// use fraiseql_core::error::Result;
335 /// use std::sync::Arc;
336 ///
337 /// struct AllowAll;
338 /// impl FieldAuthorizer for AllowAll {
339 /// fn authorize_field(&self, _req: &FieldAuthzRequest<'_>) -> Result<FieldAuthzDecision> {
340 /// Ok(FieldAuthzDecision::Allow)
341 /// }
342 /// }
343 ///
344 /// let config = RuntimeConfig::default().with_field_authorizer(Arc::new(AllowAll));
345 /// ```
346 #[must_use = "builder method returns modified builder"]
347 pub fn with_field_authorizer(mut self, authorizer: Arc<dyn FieldAuthorizer>) -> Self {
348 self.field_authorizer = Some(authorizer);
349 self
350 }
351
352 /// Configure a dynamic operation-level authorizer.
353 ///
354 /// When set, every operation (query, mutation, subscription) is passed to this
355 /// authorizer before dispatch. The decision composes as a logical AND with the
356 /// static `requires_role` gate and is fail-closed (any error or raise denies with
357 /// HTTP 403 / `FORBIDDEN`). Parallel to
358 /// [`with_field_authorizer`](Self::with_field_authorizer) and
359 /// [`with_rls_policy`](Self::with_rls_policy).
360 ///
361 /// # Example
362 ///
363 /// ```rust
364 /// use fraiseql_core::runtime::RuntimeConfig;
365 /// use fraiseql_core::security::{Authorizer, AuthzRequest, AuthzDecision};
366 /// use fraiseql_core::error::Result;
367 /// use std::sync::Arc;
368 ///
369 /// struct AllowAll;
370 /// impl Authorizer for AllowAll {
371 /// fn authorize(&self, _req: &AuthzRequest<'_>) -> Result<AuthzDecision> {
372 /// Ok(AuthzDecision::Allow)
373 /// }
374 /// }
375 ///
376 /// let config = RuntimeConfig::default().with_authorizer(Arc::new(AllowAll));
377 /// ```
378 #[must_use = "builder method returns modified builder"]
379 pub fn with_authorizer(mut self, authorizer: Arc<dyn Authorizer>) -> Self {
380 self.authorizer = Some(authorizer);
381 self
382 }
383}
384
385/// Execution context for query cancellation support.
386///
387/// This struct provides a mechanism for gracefully cancelling long-running queries
388/// via cancellation tokens, enabling proper cleanup and error reporting when:
389/// - A client connection closes
390/// - A user explicitly cancels a query
391/// - A system shutdown is initiated
392///
393/// # Example
394///
395/// ```no_run
396/// // Requires: a running tokio runtime and an Executor with a live database adapter.
397/// // See: tests/integration/ for runnable examples.
398/// use fraiseql_core::runtime::ExecutionContext;
399/// use std::time::Duration;
400///
401/// let ctx = ExecutionContext::new("query-123".to_string());
402///
403/// // Spawn a task that cancels after 5 seconds
404/// let cancel_token = ctx.cancellation_token().clone();
405/// tokio::spawn(async move {
406/// tokio::time::sleep(Duration::from_secs(5)).await;
407/// cancel_token.cancel();
408/// });
409///
410/// // Execute query with cancellation support
411/// // let result = executor.execute_with_context(query, None, &ctx).await;
412/// ```
413#[derive(Debug, Clone)]
414pub struct ExecutionContext {
415 /// Unique identifier for tracking the query execution
416 query_id: String,
417
418 /// Cancellation token for gracefully stopping the query
419 /// When cancelled, ongoing query execution should stop and return a Cancelled error
420 token: tokio_util::sync::CancellationToken,
421}
422
423impl ExecutionContext {
424 /// Create a new execution context with a cancellation token.
425 ///
426 /// # Arguments
427 ///
428 /// * `query_id` - Unique identifier for this query execution
429 ///
430 /// # Example
431 ///
432 /// ```rust
433 /// # use fraiseql_core::runtime::ExecutionContext;
434 /// let ctx = ExecutionContext::new("user-query-001".to_string());
435 /// assert_eq!(ctx.query_id(), "user-query-001");
436 /// ```
437 #[must_use]
438 pub fn new(query_id: String) -> Self {
439 Self {
440 query_id,
441 token: tokio_util::sync::CancellationToken::new(),
442 }
443 }
444
445 /// Get the query ID.
446 #[must_use]
447 pub fn query_id(&self) -> &str {
448 &self.query_id
449 }
450
451 /// Get a reference to the cancellation token.
452 ///
453 /// The returned token can be used to:
454 /// - Clone and pass to background tasks
455 /// - Check if cancellation was requested
456 /// - Propagate cancellation through the call stack
457 #[must_use]
458 pub const fn cancellation_token(&self) -> &tokio_util::sync::CancellationToken {
459 &self.token
460 }
461
462 /// Check if cancellation has been requested.
463 #[must_use]
464 pub fn is_cancelled(&self) -> bool {
465 self.token.is_cancelled()
466 }
467}
468
469#[cfg(test)]
470mod tests;