use std::{collections::HashMap, sync::Arc};
use moka::sync::Cache as MokaCache;
use super::{
context::ExecutorContext,
runners,
support::relay::{RelayDispatch, RelayDispatchImpl},
};
use crate::{
backend::{
AdminSqlOutcome, AdminSqlRequest, RelayDatabaseAdapter, ResultCacheStats,
traits::{DatabaseAdapter, Writer},
types::{DatabaseType, PoolMetrics, QueryStatEntry},
},
cache::ViewName,
error::Result,
runtime::{QueryMatcher, QueryPlanner, RuntimeConfig, matcher::QueryMatch},
schema::{CompiledSchema, IntrospectionResponses},
security::SecurityContext,
};
fn build_introspection(schema: &CompiledSchema) -> IntrospectionResponses {
#[cfg_attr(not(feature = "federation"), allow(unused_mut))]
let mut introspection = IntrospectionResponses::build(schema);
#[cfg(feature = "federation")]
if let Some(fed_meta) = schema.federation_metadata() {
let inaccessible: HashMap<String, Vec<String>> = fed_meta
.types
.iter()
.filter(|t| !t.inaccessible_fields.is_empty())
.map(|t| (t.name.clone(), t.inaccessible_fields.clone()))
.collect();
introspection.filter_inaccessible(&inaccessible);
}
introspection
}
fn resolve_gate1(
config: &RuntimeConfig,
schema: &CompiledSchema,
) -> crate::security::QueryValidator {
let effective = config.query_validation.clone().unwrap_or_else(|| {
let declared = schema.validation_config.as_ref();
let depth = declared
.and_then(|v| v.max_query_depth)
.unwrap_or(crate::schema::DEFAULT_MAX_QUERY_DEPTH);
crate::security::QueryValidatorConfig {
max_depth: depth as usize,
max_complexity: declared
.and_then(|v| v.max_query_complexity)
.map_or(usize::MAX, |c| c as usize),
max_size_bytes: usize::MAX,
max_aliases: usize::MAX,
}
});
crate::security::QueryValidator::from_config(effective)
}
const PARSE_CACHE_CAPACITY: u64 = 1_024;
const INTROSPECTION_PROJECTION_CAPACITY: u64 = 64;
const MAX_PG_IDENTIFIER_LEN: usize = 63;
pub struct Executor {
pub(super) ctx: Arc<ExecutorContext>,
}
impl Executor {
#[must_use]
pub fn new<A: Writer>(schema: CompiledSchema, adapter: Arc<A>) -> Self {
Self::with_config(schema, adapter, RuntimeConfig::default())
}
#[must_use]
pub fn with_config<A: Writer>(
schema: CompiledSchema,
adapter: Arc<A>,
config: RuntimeConfig,
) -> Self {
let writer: Arc<dyn Writer> = Arc::clone(&adapter) as Arc<dyn Writer>;
let adapter: Arc<dyn DatabaseAdapter> = adapter;
Self::build(schema, adapter, std::any::type_name::<A>(), config, None, Some(writer))
}
#[must_use]
pub fn read_only<A: DatabaseAdapter>(schema: CompiledSchema, adapter: Arc<A>) -> Self {
Self::read_only_with_config(schema, adapter, RuntimeConfig::default())
}
#[must_use]
pub fn read_only_with_config<A: DatabaseAdapter>(
schema: CompiledSchema,
adapter: Arc<A>,
config: RuntimeConfig,
) -> Self {
let adapter: Arc<dyn DatabaseAdapter> = adapter;
Self::build(schema, adapter, std::any::type_name::<A>(), config, None, None)
}
fn build(
schema: CompiledSchema,
adapter: Arc<dyn DatabaseAdapter>,
adapter_name: &'static str,
config: RuntimeConfig,
relay: Option<Arc<dyn RelayDispatch>>,
writer: Option<Arc<dyn Writer>>,
) -> Self {
let gate1 = resolve_gate1(&config, &schema);
let max_depth = u32::try_from(gate1.config().max_depth).unwrap_or(u32::MAX);
let matcher = QueryMatcher::new(schema.clone()).with_max_depth(max_depth);
let planner = QueryPlanner::new(config.cache_query_plans);
let introspection = build_introspection(&schema);
let mut node_type_index: HashMap<String, Arc<str>> = HashMap::new();
for q in &schema.queries {
if let Some(src) = q.sql_source.as_deref() {
node_type_index.entry(q.return_type.clone()).or_insert_with(|| Arc::from(src));
}
}
let schema_version: Arc<str> = Arc::from(schema.content_hash());
let nested_row_gates = super::runners::query_nested::NestedRowGates::build(
&schema,
config.rls_policy.as_deref(),
);
let ctx = Arc::new(ExecutorContext {
schema,
schema_version,
nested_row_gates,
adapter,
adapter_name,
writer,
relay,
matcher,
planner,
config,
introspection,
node_type_index,
gate1,
parse_cache: MokaCache::new(PARSE_CACHE_CAPACITY),
introspection_projections: MokaCache::new(INTROSPECTION_PROJECTION_CAPACITY),
response_cache: None,
});
Self { ctx }
}
#[must_use]
pub fn pool_metrics(&self) -> PoolMetrics {
self.ctx.pool_metrics()
}
#[must_use]
pub fn max_query_depth(&self) -> u32 {
u32::try_from(self.ctx.gate1.config().max_depth).unwrap_or(u32::MAX)
}
#[must_use]
pub fn schema(&self) -> &CompiledSchema {
&self.ctx.schema
}
#[must_use]
pub fn config(&self) -> &RuntimeConfig {
&self.ctx.config
}
#[must_use]
pub fn relay_enabled(&self) -> bool {
self.ctx.relay.is_some()
}
#[must_use]
pub fn database_type(&self) -> DatabaseType {
self.ctx.database_type()
}
pub async fn health_check(&self) -> Result<()> {
self.ctx.health_check().await
}
pub async fn query_stats(&self, limit: u32) -> Result<Vec<QueryStatEntry>> {
self.ctx.query_stats(limit).await
}
pub async fn query_stats_by_id(&self, id: &str) -> Result<Option<QueryStatEntry>> {
self.ctx.query_stats_by_id(id).await
}
pub async fn reset_query_stats(&self) -> Result<()> {
self.ctx.reset_query_stats().await
}
#[must_use]
pub fn result_cache_stats(&self) -> Option<ResultCacheStats> {
self.ctx.result_cache_stats()
}
pub async fn clear_result_cache(&self) -> Result<Option<usize>> {
self.ctx.clear_result_cache().await
}
pub async fn invalidate_views(&self, views: &[ViewName]) -> Result<u64> {
self.ctx.invalidate_views(views).await
}
pub async fn explain_query(
&self,
sql: &str,
params: &[serde_json::Value],
) -> Result<serde_json::Value> {
self.ctx.explain_query(sql, params).await
}
pub async fn execute_admin_sql(&self, request: &AdminSqlRequest) -> Result<AdminSqlOutcome> {
self.ctx.execute_admin_sql(request).await
}
pub fn on_schema_reload(&self) {
self.ctx.on_schema_reload();
}
pub async fn drop_tenant_schema(&self, schema_name: &str) -> Result<()> {
if schema_name.is_empty()
|| schema_name.len() > MAX_PG_IDENTIFIER_LEN
|| !schema_name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
{
return Err(crate::error::FraiseQLError::validation(format!(
"refusing to drop schema '{schema_name}': not a bare identifier"
)));
}
self.ctx
.execute_ddl(&format!("DROP SCHEMA IF EXISTS {schema_name} CASCADE"))
.await
}
#[must_use]
pub fn rebuild_with(&self, schema: CompiledSchema, config: RuntimeConfig) -> Self {
Self::build(
schema,
Arc::clone(&self.ctx.adapter),
self.ctx.adapter_name,
config,
self.ctx.relay.clone(),
self.ctx.writer.clone(),
)
}
#[cfg(test)]
#[must_use]
pub fn parse_cache_entry_count(&self) -> u64 {
self.ctx.parse_cache.entry_count()
}
#[must_use]
pub fn with_response_cache(mut self, cache: Arc<crate::cache::ResponseCache>) -> Self {
Arc::get_mut(&mut self.ctx)
.expect("with_response_cache called after Arc was shared")
.response_cache = Some(cache);
self
}
#[must_use]
pub fn response_cache(&self) -> Option<&Arc<crate::cache::ResponseCache>> {
self.ctx.response_cache.as_ref()
}
pub(super) const fn from_ctx(ctx: Arc<ExecutorContext>) -> Self {
Self { ctx }
}
pub(super) fn query_runner(&self) -> runners::query::QueryRunner {
runners::query::QueryRunner::new(Arc::clone(&self.ctx))
}
pub(super) fn aggregate_runner(&self) -> runners::aggregate::AggregateRunner {
runners::aggregate::AggregateRunner::new(Arc::clone(&self.ctx))
}
pub async fn execute_aggregate_query(
&self,
query_json: &serde_json::Value,
query_name: &str,
metadata: &crate::compiler::fact_table::FactTableMetadata,
) -> Result<serde_json::Value> {
if let Some(authorizer) = self.ctx.config.authorizer.as_ref() {
let ops = [super::support::authz::root_operation(
&self.ctx.schema,
crate::security::OperationKind::Query,
query_name,
)];
crate::security::authorizer::enforce_authz(
authorizer.as_ref(),
None,
&ops,
Some(query_json),
)?;
}
self.aggregate_runner()
.execute_aggregate_query(query_json, query_name, metadata, None)
.await
}
pub async fn execute_window_query(
&self,
query_json: &serde_json::Value,
query_name: &str,
metadata: &crate::compiler::fact_table::FactTableMetadata,
) -> Result<serde_json::Value> {
if let Some(authorizer) = self.ctx.config.authorizer.as_ref() {
let ops = [super::support::authz::root_operation(
&self.ctx.schema,
crate::security::OperationKind::Query,
query_name,
)];
crate::security::authorizer::enforce_authz(
authorizer.as_ref(),
None,
&ops,
Some(query_json),
)?;
}
self.aggregate_runner()
.execute_window_query(query_json, query_name, metadata, None)
.await
}
pub async fn count_rows(
&self,
query_match: &QueryMatch,
variables: Option<&serde_json::Value>,
security_context: Option<&SecurityContext>,
) -> Result<u64> {
crate::runtime::executor::support::security::enforce_enrichment_resolved(
&self.ctx.schema,
security_context,
)?;
self.query_runner().count_rows(query_match, variables, security_context).await
}
pub async fn execute_query_direct(
&self,
query_match: &QueryMatch,
variables: Option<&serde_json::Value>,
security_context: Option<&SecurityContext>,
request_budget: Option<&crate::security::RequestBudget>,
) -> Result<serde_json::Value> {
crate::runtime::executor::support::security::enforce_enrichment_resolved(
&self.ctx.schema,
security_context,
)?;
self.query_runner()
.execute_query_direct(query_match, variables, security_context, request_budget)
.await
}
#[must_use]
pub fn supports_composed_reads(&self) -> bool {
self.ctx.adapter.supports_composed_reads()
}
pub async fn execute_query_composed(
&self,
query_match: &QueryMatch,
embeds: &[crate::runtime::EmbedSelection],
counts: &[crate::runtime::CountSelection],
variables: Option<&serde_json::Value>,
security_context: Option<&SecurityContext>,
request_budget: Option<&crate::security::RequestBudget>,
) -> Result<serde_json::Value> {
crate::runtime::executor::support::security::enforce_enrichment_resolved(
&self.ctx.schema,
security_context,
)?;
self.query_runner()
.execute_query_composed(
query_match,
embeds,
counts,
variables,
security_context,
request_budget,
)
.await
}
#[must_use]
pub fn request_budget(&self) -> crate::security::RequestBudget {
crate::security::RequestBudget::new(
self.ctx.config.max_response_bytes,
self.ctx.config.max_operation_cost,
)
}
pub async fn stream_query_direct(
&self,
query_match: QueryMatch,
variables: Option<serde_json::Value>,
security_context: Option<SecurityContext>,
) -> Result<crate::runtime::JsonRowStream> {
crate::runtime::executor::support::security::enforce_enrichment_resolved(
&self.ctx.schema,
security_context.as_ref(),
)?;
self.query_runner()
.stream_query_direct(query_match, variables, security_context)
.await
}
pub async fn execute_row_read(
&self,
query_match: &QueryMatch,
variables: Option<&serde_json::Value>,
security_context: Option<&SecurityContext>,
columns: &[fraiseql_db::types::ColumnSpec],
) -> Result<super::RowRead> {
crate::runtime::executor::support::security::enforce_enrichment_resolved(
&self.ctx.schema,
security_context,
)?;
self.query_runner()
.execute_row_read(query_match, variables, security_context, columns)
.await
}
pub async fn stream_row_read(
&self,
query_match: &QueryMatch,
variables: Option<&serde_json::Value>,
security_context: Option<&SecurityContext>,
columns: &[fraiseql_db::types::ColumnSpec],
) -> Result<super::StreamedRowRead> {
crate::runtime::executor::support::security::enforce_enrichment_resolved(
&self.ctx.schema,
security_context,
)?;
self.query_runner()
.stream_row_read(query_match, variables, security_context, columns)
.await
}
}
impl Executor {
#[must_use]
pub fn new_with_relay<A: Writer + RelayDatabaseAdapter + 'static>(
schema: CompiledSchema,
adapter: Arc<A>,
) -> Self {
Self::with_config_and_relay(schema, adapter, RuntimeConfig::default())
}
#[must_use]
pub fn with_config_and_relay<A: Writer + RelayDatabaseAdapter + 'static>(
schema: CompiledSchema,
adapter: Arc<A>,
config: RuntimeConfig,
) -> Self {
let relay_dispatch: Arc<dyn RelayDispatch> =
Arc::new(RelayDispatchImpl(Arc::clone(&adapter)));
let writer: Arc<dyn Writer> = Arc::clone(&adapter) as Arc<dyn Writer>;
let adapter: Arc<dyn DatabaseAdapter> = adapter;
Self::build(
schema,
adapter,
std::any::type_name::<A>(),
config,
Some(relay_dispatch),
Some(writer),
)
}
}