mod aggregate_parser;
mod aggregate_projector;
pub mod aggregation;
pub mod cascade;
mod executor;
pub mod executor_adapter;
mod explain;
pub mod field_filter;
pub mod input_validator;
pub mod jsonb_strategy;
mod matcher;
pub mod mutation_result;
pub(crate) mod native_columns;
pub mod partial_period;
mod planner;
pub(crate) mod projection;
pub mod query_tracing;
pub mod relay;
pub mod sql_logger;
pub mod subscription;
pub mod tenant_enforcer;
pub mod window;
mod window_parser;
mod window_projector;
use std::sync::Arc;
pub use aggregate_parser::AggregateQueryParser;
pub use aggregate_projector::AggregationProjector;
pub use aggregation::{AggregationSqlGenerator, ParameterizedAggregationSql};
pub use executor::{
Executor,
pipeline::{extract_root_field_names, is_multi_root, multi_root_queries_total},
};
pub use executor_adapter::ExecutorAdapter;
pub use explain::{ExplainPlan, ExplainResult};
pub use field_filter::{FieldAccessResult, can_access_field, classify_field_access, filter_fields};
pub use jsonb_strategy::{JsonbOptimizationOptions, JsonbStrategy};
pub use matcher::{QueryMatch, QueryMatcher, suggest_similar};
pub use planner::{ExecutionPlan, QueryPlanner};
pub use projection::{FieldMapping, ProjectionMapper, ResultProjector, project_entity};
pub use query_tracing::{
QueryExecutionTrace, QueryPhaseSpan, QueryTraceBuilder, create_phase_span, create_query_span,
};
pub use sql_logger::{SqlOperation, SqlQueryLog, SqlQueryLogBuilder, create_sql_span};
pub use subscription::{
ActiveSubscription, DeliveryResult, KafkaAdapter, KafkaConfig, KafkaMessage, SubscriptionError,
SubscriptionEvent, SubscriptionId, SubscriptionManager, SubscriptionOperation,
SubscriptionPayload, TransportAdapter, TransportManager, WebhookAdapter, WebhookConfig,
WebhookPayload, extract_rls_conditions, protocol,
};
pub use tenant_enforcer::TenantEnforcer;
#[derive(Debug, Clone)]
pub struct BulkResult {
pub affected_rows: u64,
pub entities: Option<Vec<serde_json::Value>>,
}
pub use window::{WindowSql, WindowSqlGenerator};
pub use window_parser::WindowQueryParser;
pub use window_projector::WindowProjector;
use crate::security::{
Authorizer, FieldAuthorizer, FieldFilter, FieldFilterConfig, QueryValidatorConfig, RLSPolicy,
};
pub struct RuntimeConfig {
pub cache_query_plans: bool,
pub max_query_depth: usize,
pub max_query_complexity: usize,
pub max_page_size: Option<u32>,
pub enable_tracing: bool,
pub field_filter: Option<FieldFilter>,
pub rls_policy: Option<Arc<dyn RLSPolicy>>,
pub field_authorizer: Option<Arc<dyn FieldAuthorizer>>,
pub authorizer: Option<Arc<dyn Authorizer>>,
pub query_timeout_ms: u64,
pub jsonb_optimization: JsonbOptimizationOptions,
pub query_validation: Option<QueryValidatorConfig>,
pub audit_mutations: bool,
pub changelog_enabled: bool,
pub dry_run_mutations: bool,
pub cascade_limits: CascadeLimits,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct CascadeLimits {
pub max_depth: usize,
pub max_updated_entities: usize,
pub max_response_size_mb: usize,
}
impl Default for CascadeLimits {
fn default() -> Self {
Self {
max_depth: 3,
max_updated_entities: 500,
max_response_size_mb: 5,
}
}
}
impl std::fmt::Debug for RuntimeConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RuntimeConfig")
.field("cache_query_plans", &self.cache_query_plans)
.field("max_query_depth", &self.max_query_depth)
.field("max_query_complexity", &self.max_query_complexity)
.field("max_page_size", &self.max_page_size)
.field("enable_tracing", &self.enable_tracing)
.field("field_filter", &self.field_filter.is_some())
.field("rls_policy", &self.rls_policy.is_some())
.field("field_authorizer", &self.field_authorizer.is_some())
.field("authorizer", &self.authorizer.is_some())
.field("query_timeout_ms", &self.query_timeout_ms)
.field("jsonb_optimization", &self.jsonb_optimization)
.field("query_validation", &self.query_validation)
.field("audit_mutations", &self.audit_mutations)
.field("changelog_enabled", &self.changelog_enabled)
.field("dry_run_mutations", &self.dry_run_mutations)
.field("cascade_limits", &self.cascade_limits)
.finish()
}
}
impl Default for RuntimeConfig {
fn default() -> Self {
Self {
cache_query_plans: true,
max_query_depth: 10,
max_query_complexity: 1000,
max_page_size: Some(1000),
enable_tracing: false,
field_filter: None,
rls_policy: None,
field_authorizer: None,
authorizer: None,
query_timeout_ms: 30_000, jsonb_optimization: JsonbOptimizationOptions::default(),
query_validation: None,
audit_mutations: false,
changelog_enabled: true,
dry_run_mutations: false,
cascade_limits: CascadeLimits::default(),
}
}
}
impl RuntimeConfig {
#[must_use = "builder method returns modified builder"]
pub fn with_field_filter(mut self, config: FieldFilterConfig) -> Self {
self.field_filter = Some(FieldFilter::new(config));
self
}
#[must_use = "builder method returns modified builder"]
pub fn with_rls_policy(mut self, policy: Arc<dyn RLSPolicy>) -> Self {
self.rls_policy = Some(policy);
self
}
#[must_use = "builder method returns modified builder"]
pub fn with_field_authorizer(mut self, authorizer: Arc<dyn FieldAuthorizer>) -> Self {
self.field_authorizer = Some(authorizer);
self
}
#[must_use = "builder method returns modified builder"]
pub const fn with_cascade_limits(mut self, limits: CascadeLimits) -> Self {
self.cascade_limits = limits;
self
}
#[must_use = "builder method returns modified builder"]
pub fn with_authorizer(mut self, authorizer: Arc<dyn Authorizer>) -> Self {
self.authorizer = Some(authorizer);
self
}
pub fn from_compiled_schema(schema: &crate::schema::CompiledSchema) -> Result<Self, String> {
if schema.schema_format_version.is_none() {
tracing::warn!(
"Loaded schema has no schema_format_version (pre-v2.1 format). \
Re-compile with the current fraiseql-cli for version compatibility checking."
);
}
schema.validate_format_version()?;
let audit_mutations = schema
.security
.as_ref()
.and_then(|s| s.additional.get("enterprise"))
.and_then(|e| e.get("audit_logging_enabled"))
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
if audit_mutations {
tracing::info!("Mutation audit logging enabled (target: fraiseql::mutation_audit)");
}
let max_page_size = page_size_precedence(
std::env::var("FRAISEQL_MAX_PAGE_SIZE").ok().as_deref(),
schema.validation_config.as_ref().and_then(|v| v.max_page_size),
);
let changelog_enabled = std::env::var("FRAISEQL_CHANGELOG_ENABLED")
.ok()
.map(|v| {
!matches!(v.trim().to_ascii_lowercase().as_str(), "false" | "0" | "no" | "off")
})
.or_else(|| schema.changelog.as_ref().map(|c| c.write_enabled))
.unwrap_or(true);
if !changelog_enabled {
tracing::info!(
"Change-log outbox write disabled (FRAISEQL_CHANGELOG_ENABLED / [changelog] write_enabled)"
);
}
Ok(Self {
max_page_size,
audit_mutations,
changelog_enabled,
..Self::default()
})
}
}
#[must_use]
pub fn page_size_precedence(env: Option<&str>, compiled: Option<u32>) -> Option<u32> {
if let Some(raw) = env {
let trimmed = raw.trim();
if trimmed.eq_ignore_ascii_case("none") || trimmed == "0" {
return None;
}
if let Ok(n) = trimmed.parse::<u32>() {
return Some(n);
}
}
compiled.or(RuntimeConfig::default().max_page_size)
}
#[derive(Debug, Clone)]
pub struct ExecutionContext {
query_id: String,
token: tokio_util::sync::CancellationToken,
}
impl ExecutionContext {
#[must_use]
pub fn new(query_id: String) -> Self {
Self {
query_id,
token: tokio_util::sync::CancellationToken::new(),
}
}
#[must_use]
pub fn query_id(&self) -> &str {
&self.query_id
}
#[must_use]
pub const fn cancellation_token(&self) -> &tokio_util::sync::CancellationToken {
&self.token
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.token.is_cancelled()
}
}
#[cfg(test)]
mod tests;