mod aggregate_parser;
mod aggregate_projector;
pub mod aggregation;
mod argument_validation;
mod argument_value_validation;
pub mod cascade;
mod embed_selection;
mod executor;
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_function;
pub mod query_tracing;
pub mod relay;
pub mod sql_logger;
pub mod subscription;
pub mod tenant_enforcer;
mod variable_validation;
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 argument_validation::validate_argument_names;
pub use argument_value_validation::{
validate_argument_values, validate_enum_argument_literals, validate_enum_argument_values,
validate_enum_variable_values, validate_variable_values,
};
pub use embed_selection::{CountSelection, EmbedSelection};
pub(crate) use executor::field_type_to_where_type;
pub use executor::{
Executor,
JsonRowStream,
MutationExecution,
RowRead,
StampContract,
StreamedRowRead,
SubscriptionPlan,
coerce_pagination_arg,
mutation::{WriteSelections, mutation_return_selections},
mutation_contract_errors,
pipeline::{extract_root_field_names, is_multi_root, multi_root_queries_total},
security::resolve_session_variables,
suppressed_subscription_events,
};
pub use explain::{ExplainPlan, ExplainResult};
pub use field_filter::{
FieldAccessResult, can_access_field, can_reference_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(crate) use projection::stored_key_candidates;
pub use projection::{
FieldMapping, ProjectionMapper, ResultProjector, project_entity, project_nested_lists,
stamp_nested_typenames,
};
pub use query_function::{QueryFunctionRequest, QueryFunctionResolver};
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;
pub use variable_validation::{
collect_variable_references, validate_variable_types, validate_variable_uses,
validate_variables_used,
};
#[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, BeforeMutationGate, FieldAuthorizer, FieldFilter, FieldFilterConfig,
QueryValidatorConfig, RLSPolicy,
};
#[derive(Clone)]
pub struct RuntimeConfig {
pub cache_query_plans: bool,
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 max_operation_cost: Option<u64>,
pub max_response_bytes: Option<u64>,
pub audit_mutations: bool,
pub changelog_enabled: bool,
pub dry_run_mutations: bool,
pub cascade_limits: CascadeLimits,
pub before_mutation_gate: Option<Arc<dyn BeforeMutationGate>>,
pub query_function_resolver: Option<Arc<dyn QueryFunctionResolver>>,
}
#[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_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("max_operation_cost", &self.max_operation_cost)
.field("max_response_bytes", &self.max_response_bytes)
.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)
.field("before_mutation_gate", &self.before_mutation_gate.is_some())
.field("query_function_resolver", &self.query_function_resolver.is_some())
.finish()
}
}
impl Default for RuntimeConfig {
fn default() -> Self {
Self {
cache_query_plans: true,
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,
max_operation_cost: None,
max_response_bytes: None,
audit_mutations: false,
changelog_enabled: true,
dry_run_mutations: false,
cascade_limits: CascadeLimits::default(),
before_mutation_gate: None,
query_function_resolver: None,
}
}
}
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
}
#[must_use = "builder method returns modified builder"]
pub fn with_before_mutation_gate(mut self, gate: Arc<dyn BeforeMutationGate>) -> Self {
self.before_mutation_gate = Some(gate);
self
}
#[must_use]
pub fn with_query_function_resolver(
mut self,
resolver: Arc<dyn QueryFunctionResolver>,
) -> Self {
self.query_function_resolver = Some(resolver);
self
}
pub fn from_compiled_schema(schema: &crate::schema::CompiledSchema) -> Result<Self, String> {
Self::default().with_compiled_schema(schema)
}
pub fn with_compiled_schema(
self,
schema: &crate::schema::CompiledSchema,
) -> Result<Self, String> {
schema.validate_producer_version()?;
let audit_mutations = schema
.security
.as_ref()
.and_then(|s| s.enterprise.as_ref())
.is_some_and(|e| e.audit_logging_enabled);
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)"
);
}
let max_operation_cost = schema
.security
.as_ref()
.and_then(|s| s.cost_budget.as_ref())
.and_then(|c| c.per_request_max);
let max_response_bytes =
schema.validation_config.as_ref().and_then(|v| v.max_response_bytes);
let Self {
cache_query_plans,
max_page_size: _, enable_tracing,
field_filter,
rls_policy,
field_authorizer,
authorizer,
query_timeout_ms,
jsonb_optimization,
query_validation,
max_operation_cost: _, max_response_bytes: _, audit_mutations: _, changelog_enabled: _, dry_run_mutations,
cascade_limits,
before_mutation_gate,
query_function_resolver,
} = self;
Ok(Self {
cache_query_plans,
max_page_size,
enable_tracing,
field_filter,
rls_policy,
field_authorizer,
authorizer,
query_timeout_ms,
jsonb_optimization,
query_validation,
max_operation_cost,
max_response_bytes,
audit_mutations,
changelog_enabled,
dry_run_mutations,
cascade_limits,
before_mutation_gate,
query_function_resolver,
})
}
}
#[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;