use std::collections::HashMap;
use std::sync::Arc;
use parking_lot::RwLock;
use super::engine::StreamingExecutionEngine;
use super::plan::materializer::PhysicalPlanMaterializer;
use super::plan::types::{PhysicalOperatorId, PhysicalPlan};
use super::plan::validator::PhysicalPlanValidator;
use super::result_utils::convert_chunks_to_dataset_with_spill;
use super::runtime::ExecutionRuntime;
use super::stream_result::StreamingQueryResult;
use crate::executor::base::{ExecutionResult, MemoryBudget};
use crate::optimizer::stats::feedback::history::QueryFeedbackHistory;
use crate::optimizer::stats::feedback::query::{OperatorFeedback, QueryExecutionFeedback};
use crate::storage::QueryStorage;
use linkrs_core::error::QueryError;
use linkrs_core::Arena;
use linkrs_core::Value;
use super::parameters::{ParameterFrame, ParameterSchema};
use super::query_registry::{CancelToken, QueryGuard, QueryId, QueryMetadata, QueryRegistry};
use super::transaction_scope::TransactionScope;
use crate::query_manager::QueryManager;
#[derive(Clone)]
pub struct QueryBindings {
pub parameters: Arc<HashMap<String, Value>>,
pub session_variables: Arc<HashMap<String, Value>>,
pub parameter_frame: Option<ParameterFrame>,
pub space_name: Option<String>,
pub storage: Option<Arc<RwLock<dyn QueryStorage>>>,
pub bound_snapshot: Option<crate::storage::SnapshotHandle>,
pub memory_budget: MemoryBudget,
pub max_workers: usize,
pub chunk_size: usize,
pub max_buffered_chunks: usize,
pub query_id: u64,
pub cancel_token: Option<CancelToken>,
pub query_text: Option<String>,
pub session_id: Option<String>,
pub user_name: Option<String>,
pub transaction: TransactionScope,
pub shared_scheduler: Option<Arc<super::pool::SharedScheduler>>,
pub partition_count: usize,
pub arena: Option<Arc<parking_lot::Mutex<Arena>>>,
pub feedback_history: Option<Arc<QueryFeedbackHistory>>,
pub columnar_policy: Option<Arc<super::chunk::ColumnarPolicy>>,
pub macro_manager: Option<Arc<linkrs_core::metadata::MacroManager>>,
pub type_alias_manager: Option<Arc<linkrs_core::metadata::TypeAliasManager>>,
pub sequence_manager: Option<Arc<linkrs_core::metadata::SequenceManager>>,
pub search: crate::executor::base::SearchContext,
}
impl std::fmt::Debug for QueryBindings {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("QueryBindings")
.field("parameters", &self.parameters)
.field("space_name", &self.space_name)
.field("query_id", &self.query_id)
.finish_non_exhaustive()
}
}
impl QueryBindings {
pub fn from_context(
context: &crate::executor::base::ExecutionContext,
transaction: TransactionScope,
) -> Self {
Self {
parameters: context.parameters.clone(),
session_variables: context.session_variables.clone(),
parameter_frame: None,
space_name: context.space_name.clone(),
storage: context.storage.clone(),
bound_snapshot: context.bound_snapshot,
memory_budget: context.memory_budget.clone(),
max_workers: context.max_workers,
chunk_size: context.chunk_size,
max_buffered_chunks: context.max_buffered_chunks,
query_id: context.query_id,
cancel_token: context.cancel_token.clone(),
query_text: None,
session_id: None,
user_name: None,
transaction,
shared_scheduler: context.shared_scheduler.clone(),
partition_count: 0,
arena: context.arena.clone(),
feedback_history: context.feedback_history.clone(),
columnar_policy: context.columnar_policy.clone(),
macro_manager: context.macro_manager.clone(),
type_alias_manager: context.type_alias_manager.clone(),
sequence_manager: context.sequence_manager.clone(),
search: context.search.clone(),
}
}
pub fn build_parameter_frame(&mut self, schema: &ParameterSchema) {
let mut values = Vec::with_capacity(schema.params.len());
for param in &schema.params {
let value = self
.parameters
.get(¶m.name)
.cloned()
.or_else(|| param.default.clone())
.unwrap_or(linkrs_core::Value::Null(Default::default()));
values.push(value);
}
self.parameter_frame = Some(ParameterFrame::new(values));
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResultSink {
Materialize,
Stream,
Discard,
}
struct AggregatedOperatorProfile {
name: String,
output_rows: u64,
input_rows: u64,
time_us: u64,
loops: u64,
partitions: u32,
exhausted: bool,
}
impl AggregatedOperatorProfile {
fn default() -> Self {
Self {
name: String::new(),
output_rows: 0,
input_rows: 0,
time_us: 0,
loops: 0,
partitions: 0,
exhausted: true,
}
}
}
pub struct QueryExecutionInstance {
plan: Arc<PhysicalPlan>,
_bindings: QueryBindings,
runtime: Arc<ExecutionRuntime>,
engine: Option<StreamingExecutionEngine>,
sink: ResultSink,
_registry_guard: Option<QueryGuard>,
}
impl QueryExecutionInstance {
pub fn instantiate_plan(
plan: Arc<PhysicalPlan>,
bindings: QueryBindings,
sink: ResultSink,
registry: Option<Arc<QueryRegistry>>,
query_manager: Option<Arc<QueryManager>>,
) -> Result<Self, QueryError> {
PhysicalPlanValidator::validate(&plan)?;
let (executor, runtime) = PhysicalPlanMaterializer::materialize(&plan, &bindings)?;
runtime.set_cancel_token(bindings.cancel_token.clone().unwrap_or_default());
let registry_guard = registry.as_ref().map(|reg| {
let session_id = bindings
.session_id
.as_ref()
.and_then(|s| s.parse::<i64>().ok());
let meta = QueryMetadata {
query_id: QueryId(bindings.query_id.max(1)),
session_id,
user_name: bindings.user_name.clone(),
space_name: bindings.space_name.clone(),
query_text: bindings.query_text.clone(),
start_time: std::time::Instant::now(),
};
let (qid, token) = if bindings.query_id != 0 {
reg.register_with_token(
QueryId(bindings.query_id),
meta,
bindings.cancel_token.clone(),
)
} else {
reg.register_with_token(QueryId(0), meta, bindings.cancel_token.clone())
};
runtime.assign_query_id(qid.as_u64());
runtime.set_query_registry(reg.clone(), qid);
if let Some(qm) = query_manager {
runtime.set_query_manager(qm);
}
runtime.set_cancel_token(token);
QueryGuard::new(reg.clone(), qid)
});
let mut engine = StreamingExecutionEngine::new();
engine.set_max_workers(bindings.max_workers);
engine.set_max_buffered_chunks(bindings.max_buffered_chunks);
engine.set_runtime(runtime.clone());
engine.register_executor(0, executor);
Ok(Self {
plan,
_bindings: bindings,
runtime,
engine: Some(engine),
sink,
_registry_guard: registry_guard,
})
}
pub fn runtime(&self) -> &Arc<ExecutionRuntime> {
&self.runtime
}
pub fn output_contract(&self) -> &super::plan::types::OutputContract {
&self.plan.output
}
pub fn fragment_graph(&self) -> &super::plan::types::FragmentGraph {
&self.plan.fragments
}
pub fn execute(&mut self) -> Result<ExecutionResult, QueryError> {
assert!(
matches!(self.sink, ResultSink::Materialize),
"QueryExecutionInstance::execute requires Materialize sink"
);
let mut engine = self
.engine
.take()
.ok_or_else(|| QueryError::execution("Engine already consumed".to_string()))?;
let chunks = engine.execute_collected()?;
self.collect_execution_feedback();
self.runtime.flush_columnar_stats_to_policy();
let spill_manager = self.runtime.get_spill_manager();
let (dataset, spilled_rows, spilled_bytes, spilled_runs) =
convert_chunks_to_dataset_with_spill(
chunks,
Some(self.plan.output.output_layout.names()),
spill_manager,
)?;
if spilled_rows > 0 {
self.runtime.columnar_stats().record_spill_with_runs(
spilled_rows,
spilled_bytes,
spilled_runs,
);
}
Ok(ExecutionResult::DataSet { data: dataset })
}
fn collect_execution_feedback(&self) {
let Some(history) = self.runtime().feedback_history.clone() else {
return;
};
let profile = self.runtime().profile().flush_to_collector();
let fingerprint = &self.plan.compatibility.fingerprint;
let mut feedback =
QueryExecutionFeedback::new(format!("v{}:{}", fingerprint.version, fingerprint.hash));
feedback.space = self._bindings.space_name.clone();
feedback.estimated_rows = self
.plan
.fragments
.get(self.plan.root_fragment)
.and_then(|fragment| self.plan.operator(fragment.root_operator))
.and_then(|operator| operator.estimated_cardinality)
.map(|rows| rows as u64)
.unwrap_or(0);
feedback.actual_rows = profile.total_rows;
feedback.actual_time_us = profile.total_time_us;
let mut aggregates: HashMap<PhysicalOperatorId, AggregatedOperatorProfile> = HashMap::new();
for (key, op_profile) in &profile.operators {
let op_time_us =
op_profile.open_time_us + op_profile.next_time_us + op_profile.close_time_us;
match op_profile.name.as_str() {
"PatternApply" => {
feedback.apply_rows += op_profile.output_rows;
feedback.apply_time_us += op_time_us;
}
"SemiJoin" => {
feedback.join_rows += op_profile.output_rows;
feedback.join_time_us += op_time_us;
}
_ => {}
}
let aggregate = aggregates
.entry(key.physical_operator_id)
.or_insert_with(AggregatedOperatorProfile::default);
aggregate.output_rows += op_profile.output_rows;
aggregate.input_rows += op_profile.input_rows;
aggregate.time_us += op_time_us;
aggregate.loops += op_profile.advance_count.max(1);
aggregate.partitions += 1;
aggregate.exhausted = aggregate.exhausted && op_profile.exhausted;
if aggregate.name.is_empty() {
aggregate.name = op_profile.name.clone();
}
}
for (physical_id, aggregate) in &aggregates {
if let Some(operator) = self.plan.operator(*physical_id) {
if let Some(estimated) = operator.estimated_cardinality {
let condition_key = match &operator.spec {
super::plan::types::OperatorKindSpec::Unary(
super::operators::spec::UnarySpec::Filter { predicate, .. },
) => Some(crate::optimizer::cost::selectivity::condition_key(
feedback.space.as_deref(),
predicate,
)),
_ => None,
};
let shape_key = if condition_key.is_some() {
None
} else {
crate::executor::streaming::operators::spec::operator_cardinality_shape_key(
feedback.space.as_deref(),
&operator.spec,
)
};
feedback.add_operator_feedback(OperatorFeedback {
operator_id: physical_id.0.to_string(),
operator_type: aggregate.name.clone(),
estimated_rows: estimated as u64,
actual_rows: aggregate.output_rows,
actual_input_rows: Some(aggregate.input_rows),
partition_count: aggregate.partitions,
exhausted: aggregate.exhausted,
estimated_time_us: 0,
actual_time_us: aggregate.time_us,
execution_loops: aggregate.loops,
condition_key,
shape_key,
});
}
}
}
history.add_feedback(feedback);
}
pub fn into_stream(mut self) -> Result<StreamingQueryResult, QueryError> {
assert!(
matches!(self.sink, ResultSink::Stream),
"QueryExecutionInstance::into_stream requires Stream sink"
);
let engine = self
.engine
.take()
.ok_or_else(|| QueryError::execution("Engine already consumed".to_string()))?;
let stream = engine.execute()?;
let result = StreamingQueryResult::new_with_schema(
stream,
self.runtime.clone(),
self.plan.output.output_layout.names(),
);
let runtime = self.runtime.clone();
result.set_on_drop(Box::new(move || runtime.flush_columnar_stats_to_policy()));
Ok(result)
}
pub fn execute_discard(&mut self) -> Result<(), QueryError> {
assert!(
matches!(self.sink, ResultSink::Discard),
"QueryExecutionInstance::execute_discard requires Discard sink"
);
let engine = self
.engine
.take()
.ok_or_else(|| QueryError::execution("Engine already consumed".to_string()))?;
let mut stream = engine.execute()?;
while stream.next_chunk()?.is_some() {}
stream.close()?;
self.runtime.flush_columnar_stats_to_policy();
Ok(())
}
pub fn cancel(&self) {
self.runtime.cancel();
}
}
impl std::fmt::Debug for QueryExecutionInstance {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("QueryExecutionInstance")
.field("plan_operators", &self.plan.operator_count())
.field("fragments", &self.plan.fragment_count())
.field("sink", &self.sink)
.finish()
}
}