Skip to main content

linkrs_query/pipeline/
execution.rs

1use super::QueryPipelineManager;
2use crate::executor::base::{ExecutionContext, ExecutionResult};
3use crate::executor::streaming::instance::{QueryBindings, QueryExecutionInstance, ResultSink};
4use crate::executor::streaming::plan::PhysicalPlan;
5use crate::executor::streaming::transaction_scope::TransactionScope;
6use crate::executor::streaming::StreamingQueryResult;
7use crate::storage::QueryStorage;
8use crate::QueryContext;
9use crate::QueryRequestContext;
10use linkrs_core::error::{DBError, DBResult, QueryError};
11use linkrs_core::types::SpaceInfo;
12use linkrs_core::types::TransactionId;
13use linkrs_metrics::{
14    ErrorInfo, ErrorType, MetricType, QueryMetrics, QueryPhase, QueryProfile, StatsManager,
15};
16use parking_lot::RwLock;
17use std::sync::Arc;
18use std::time::Instant;
19
20impl<S: QueryStorage + 'static> QueryPipelineManager<S> {
21    pub fn execute_query(&mut self, query_text: &str) -> DBResult<ExecutionResult> {
22        self.execute_query_with_space(query_text, None)
23    }
24
25    /// Convenience entry: parse, bind auto-commit storage for DML, compile,
26    /// execute, and finalize in one call.  Used by tests and the embedded API.
27    ///
28    /// Finalization of the auto-bound operation storage is performed by
29    /// [`execute_prepared_materialized`].
30    pub fn execute_query_with_space(
31        &mut self,
32        query_text: &str,
33        space_info: Option<SpaceInfo>,
34    ) -> DBResult<ExecutionResult> {
35        let request = self.prepare_request_with_auto_commit(query_text, space_info)?;
36        match self.execute_prepared(&request, None, ResultSink::Materialize)? {
37            super::prepared::PreparedOutcome::Materialized(result) => Ok(result),
38            _ => unreachable!("materialize sink cannot stream"),
39        }
40    }
41
42    pub fn execute_query_stream_with_request(
43        &mut self,
44        query_text: &str,
45        rctx: Arc<QueryRequestContext>,
46        space_info: Option<SpaceInfo>,
47    ) -> DBResult<StreamingQueryResult> {
48        self.execute_query_stream_with_request_scope(query_text, rctx, space_info, None)
49    }
50
51    pub fn execute_query_stream_with_request_scope(
52        &mut self,
53        query_text: &str,
54        rctx: Arc<QueryRequestContext>,
55        space_info: Option<SpaceInfo>,
56        transaction_id: Option<TransactionId>,
57    ) -> DBResult<StreamingQueryResult> {
58        let request = self.prepare_request(query_text, rctx, space_info)?;
59        match self.execute_prepared(&request, transaction_id, ResultSink::Stream)? {
60            super::prepared::PreparedOutcome::Stream(stream) => Ok(stream),
61            _ => unreachable!("stream sink cannot materialize"),
62        }
63    }
64
65    pub fn execute_query_with_request(
66        &mut self,
67        query_text: &str,
68        rctx: Arc<QueryRequestContext>,
69        space_info: Option<SpaceInfo>,
70    ) -> DBResult<ExecutionResult> {
71        self.execute_query_with_request_scope(query_text, rctx, space_info, None)
72    }
73
74    pub fn execute_query_with_request_scope(
75        &mut self,
76        query_text: &str,
77        rctx: Arc<QueryRequestContext>,
78        space_info: Option<SpaceInfo>,
79        transaction_id: Option<TransactionId>,
80    ) -> DBResult<ExecutionResult> {
81        let request = self.prepare_request(query_text, rctx, space_info)?;
82        match self.execute_prepared(&request, transaction_id, ResultSink::Materialize)? {
83            super::prepared::PreparedOutcome::Materialized(result) => Ok(result),
84            _ => unreachable!("materialize sink cannot stream"),
85        }
86    }
87
88    /// Execute a statement end to end and report its stage profile.
89    ///
90    /// The only entry that produces a [`QueryProfile`]: stage timings and the
91    /// metric counters are recorded here so embedded and API callers get the
92    /// same picture the transport layer shows, with no second profile
93    /// assembly anywhere else. `space_info` selects the target space and is
94    /// `None` for statements that do not need one.
95    pub fn execute_query_with_profile(
96        &mut self,
97        query_text: &str,
98        session_id: i64,
99        space_info: Option<SpaceInfo>,
100    ) -> DBResult<(ExecutionResult, QueryMetrics, QueryProfile)> {
101        self.stats_manager.add_value(MetricType::NumQueries);
102        self.stats_manager.add_value(MetricType::NumActiveQueries);
103
104        struct ActiveQueryGuard {
105            stats_manager: Arc<StatsManager>,
106        }
107        impl Drop for ActiveQueryGuard {
108            fn drop(&mut self) {
109                self.stats_manager.dec_value(MetricType::NumActiveQueries);
110            }
111        }
112        let _guard = ActiveQueryGuard {
113            stats_manager: self.stats_manager.clone(),
114        };
115
116        let total_start = Instant::now();
117        let mut metrics = QueryMetrics::new();
118        let mut profile = QueryProfile::new(session_id, query_text.to_string());
119
120        // Use prepared lifecycle for parse + validate
121        let prepare_start = Instant::now();
122        let request = match self.prepare_request_with_auto_commit(query_text, space_info) {
123            Ok(req) => {
124                profile.stages.parse_us = prepare_start.elapsed().as_micros() as u64;
125                profile.stages.validate_us = 0;
126                metrics.record_parse_time(prepare_start.elapsed());
127                req
128            }
129            Err(e) => {
130                profile.stages.parse_us = prepare_start.elapsed().as_micros() as u64;
131                let error_info =
132                    ErrorInfo::new(ErrorType::ParseError, QueryPhase::Parse, e.to_string());
133                profile.mark_failed_with_info(error_info.clone());
134                profile.total_duration_us = total_start.elapsed().as_micros() as u64;
135                self.stats_manager
136                    .record_failed_query(profile.clone(), error_info);
137                return Err(e);
138            }
139        };
140
141        self.record_query_type_counter(&request.stmt);
142
143        // Dedicated compile + execute: `execute_prepared` runs the generated
144        // diagnostic/analyze/DDL paths internally and uses the shared plan
145        // cache.  Compilation is folded into the execute phase for profiling
146        // (optimization happens inside the cache-compile step).
147        let execute_start = Instant::now();
148        let result = match self.execute_prepared(&request, None, ResultSink::Materialize) {
149            Ok(super::prepared::PreparedOutcome::Materialized(result)) => result,
150            Ok(_) => unreachable!("materialize sink cannot stream"),
151            Err(e) => {
152                profile.stages.execute_us = execute_start.elapsed().as_micros() as u64;
153                let error_info = ErrorInfo::new(
154                    ErrorType::ExecutionError,
155                    QueryPhase::Execute,
156                    e.to_string(),
157                );
158                profile.mark_failed_with_info(error_info.clone());
159                profile.total_duration_us = total_start.elapsed().as_micros() as u64;
160                self.stats_manager
161                    .record_failed_query(profile.clone(), error_info);
162                return Err(e);
163            }
164        };
165
166        profile.stages.execute_us = execute_start.elapsed().as_micros() as u64;
167        profile.result_count = result.count();
168        metrics.set_result_row_count(result.count());
169        metrics.record_execute_time(execute_start.elapsed());
170
171        profile.total_duration_us = total_start.elapsed().as_micros() as u64;
172        metrics.record_total_time(total_start.elapsed());
173
174        self.stats_manager.record_query_metrics(&metrics);
175        self.stats_manager.record_query_profile(profile.clone());
176
177        Ok((result, metrics, profile))
178    }
179
180    pub(crate) fn execute_compiled_with_scope(
181        &self,
182        physical_plan: Arc<PhysicalPlan>,
183        query_context: Arc<QueryContext>,
184        sink: ResultSink,
185        transaction_scope: TransactionScope,
186    ) -> DBResult<ExecutionResult> {
187        if matches!(sink, ResultSink::Stream) {
188            return Err(DBError::from(QueryError::execution(
189                "Use execute_compiled_stream_with_scope for streaming sink".to_string(),
190            )));
191        }
192
193        let exec_ctx = self.build_execution_context(&query_context);
194        validate_snapshot_consistency(&query_context, &exec_ctx)?;
195        reject_writes_outside_transaction_scope(&physical_plan, &transaction_scope)?;
196        let is_command_scope = matches!(transaction_scope, TransactionScope::CommandScope);
197        let mut bindings = QueryBindings::from_context(&exec_ctx, transaction_scope);
198        bindings.query_id = exec_ctx.query_id;
199        bindings.query_text = Some(query_context.request_context().query.clone());
200        bindings.session_id = query_context
201            .request_context()
202            .session_id
203            .map(|id| id.to_string());
204        bindings.user_name = query_context.request_context().user_name.clone();
205
206        let mut instance = QueryExecutionInstance::instantiate_plan(
207            physical_plan,
208            bindings,
209            sink,
210            self.query_registry.clone(),
211            self.query_manager.clone(),
212        )
213        .map_err(|e| DBError::from(QueryError::execution(e.to_string())))?;
214
215        if is_command_scope {
216            let mut ctrl_guard = self.session_controller.write();
217            let controller = if ctrl_guard.as_ref().is_some_and(|c| c.is_active()) {
218                ctrl_guard.clone().unwrap()
219            } else {
220                let ctrl =
221                    Arc::new(crate::executor::streaming::SessionTransactionController::new());
222                if let Some(txn_id) = query_context.request_context().transaction_id {
223                    let read_write = !query_context.request_context().read_only;
224                    ctrl.begin_tracking(txn_id, read_write)
225                        .map_err(|error| DBError::from(QueryError::execution(error.to_string())))?;
226                }
227                *ctrl_guard = Some(ctrl.clone());
228                ctrl
229            };
230            drop(ctrl_guard);
231            instance.runtime().set_session_controller(controller);
232        }
233
234        match sink {
235            ResultSink::Materialize => instance
236                .execute()
237                .map_err(|e| DBError::from(QueryError::execution(e.to_string()))),
238            ResultSink::Discard => {
239                instance
240                    .execute_discard()
241                    .map_err(|e| DBError::from(QueryError::execution(e.to_string())))?;
242                Ok(ExecutionResult::Empty)
243            }
244            ResultSink::Stream => unreachable!("stream sink is rejected before instantiation"),
245        }
246    }
247
248    pub(crate) fn execute_compiled_stream_with_scope(
249        &self,
250        physical_plan: Arc<PhysicalPlan>,
251        query_context: Arc<QueryContext>,
252        transaction_scope: TransactionScope,
253    ) -> DBResult<StreamingQueryResult> {
254        let exec_ctx = self.build_execution_context(&query_context);
255        validate_snapshot_consistency(&query_context, &exec_ctx)?;
256        reject_writes_outside_transaction_scope(&physical_plan, &transaction_scope)?;
257        let mut bindings = QueryBindings::from_context(&exec_ctx, transaction_scope);
258        bindings.query_id = exec_ctx.query_id;
259        bindings.query_text = Some(query_context.request_context().query.clone());
260        bindings.session_id = query_context
261            .request_context()
262            .session_id
263            .map(|id| id.to_string());
264        bindings.user_name = query_context.request_context().user_name.clone();
265
266        let instance = QueryExecutionInstance::instantiate_plan(
267            physical_plan,
268            bindings,
269            ResultSink::Stream,
270            self.query_registry.clone(),
271            self.query_manager.clone(),
272        )
273        .map_err(|e| DBError::from(QueryError::execution(e.to_string())))?;
274
275        instance
276            .into_stream()
277            .map_err(|e| DBError::from(QueryError::execution(e.to_string())))
278    }
279
280    pub(crate) fn build_execution_context(&self, query_context: &QueryContext) -> ExecutionContext {
281        use std::collections::HashMap;
282
283        let params: HashMap<String, linkrs_core::Value> =
284            query_context.request_context().parameters.clone();
285        let session_variables: HashMap<String, linkrs_core::Value> =
286            query_context.request_context().session_variables.clone();
287
288        let mut context = ExecutionContext {
289            max_workers: self
290                .optimizer_engine
291                .partitioning_config()
292                .max_workers
293                .max(1),
294            max_buffered_chunks: self
295                .optimizer_engine
296                .partitioning_config()
297                .max_buffered_chunks
298                .max(1),
299            parameters: Arc::new(params),
300            session_variables: Arc::new(session_variables),
301            ..ExecutionContext::default()
302        };
303        context.shared_scheduler = self.shared_scheduler.clone();
304        context.macro_manager = Some(self.macro_manager.clone());
305        context.type_alias_manager = Some(self.type_alias_manager.clone());
306        context.sequence_manager = Some(self.sequence_manager.clone());
307        if let Some(ref storage) = self.storage {
308            let dyn_storage: Arc<RwLock<dyn QueryStorage>> = if let Some(operation_storage) =
309                query_context.request_context().operation_storage.clone()
310            {
311                operation_storage
312            } else if let Some(operation) =
313                query_context.request_context().operation_context.clone()
314            {
315                let bound_storage = storage.read().bind_operation_context(operation);
316                Arc::new(RwLock::new(bound_storage))
317            } else {
318                storage.clone()
319            };
320            // surface the snapshot pinned by the per-query bound storage.
321            // Read-only statements bind a fixed read timestamp; DML binds its
322            // write timestamp. Unbound raw storage reports None.
323            context.bound_snapshot = dyn_storage.read().snapshot_handle();
324            context.storage = Some(dyn_storage);
325        }
326        #[cfg(feature = "fulltext")]
327        {
328            context.search.fulltext_manager = self.fulltext_manager.clone();
329        }
330        #[cfg(feature = "vector")]
331        {
332            context.search.vector_coordinator = self.vector_coordinator.clone();
333        }
334        // Populate unified search providers for discovery
335        {
336            #[cfg_attr(not(any(feature = "fulltext", feature = "vector")), allow(unused_mut))]
337            let mut providers: Vec<
338                Arc<dyn crate::executor::base::traits::SearchProvider>,
339            > = Vec::new();
340            #[cfg(feature = "fulltext")]
341            if let Some(ref manager) = self.fulltext_manager {
342                providers.push(Arc::new(
343                    crate::executor::base::traits::FulltextProvider::new(manager.clone()),
344                ));
345            }
346            #[cfg(feature = "vector")]
347            if let Some(ref coordinator) = self.vector_coordinator {
348                providers.push(Arc::new(
349                    crate::executor::base::traits::VectorProvider::new(coordinator.clone()),
350                ));
351            }
352            context.search.search_providers = providers;
353        }
354        if let Some(ref space_name) = query_context.space_name() {
355            context.space_name = Some(space_name.clone());
356        }
357        if let Some(query_id) = query_context.request_context().query_id {
358            context.query_id = query_id;
359        }
360        context.cancel_token = Some(query_context.cancel_token());
361        context.isolation_level = query_context.isolation_level();
362        context.ryw_config = query_context.request_context().ryw_config;
363        if query_context.has_arena() {
364            context.arena = Some(Arc::new(parking_lot::Mutex::new(linkrs_core::Arena::new())));
365        }
366        // Stats feedback loop: share the engine's feedback history
367        // with every execution so estimated-vs-actual operator feedback is
368        // recorded after the query completes.
369        context.feedback_history = Some(self.optimizer_engine.feedback_history());
370        // Columnar auto-detection: share the engine's policy so the
371        // typed columnar layout adapts to the learned hit rate; each query
372        // merges its columnar stats back at completion.
373        context.columnar_policy = Some(self.optimizer_engine.columnar_policy());
374        context
375    }
376}
377
378/// Validate that the query context and the bound storage agree on the MVCC
379/// snapshot for explicit-transaction reads.
380///
381/// All storage access of one execution instance must observe a single
382/// snapshot (blocking-operator rescans, CTE reuse, correlated-apply
383/// re-execution included). This check centralizes the invariant at runtime
384/// construction instead of trusting every operator to pin the same timestamp.
385fn validate_snapshot_consistency(
386    query_context: &QueryContext,
387    exec_ctx: &ExecutionContext,
388) -> DBResult<()> {
389    let Some(snapshot_ts) = query_context.snapshot_ts() else {
390        return Ok(());
391    };
392    match exec_ctx.bound_snapshot {
393        Some(handle) if handle.ts != snapshot_ts => {
394            Err(DBError::from(QueryError::execution(format!(
395                "snapshot consistency violation: query context pins ts {snapshot_ts} \
396                 but bound storage reads ts {}",
397                handle.ts
398            ))))
399        }
400        Some(_) => Ok(()),
401        None => {
402            log::debug!(
403                "bound storage did not expose a snapshot handle for pinned ts {snapshot_ts}; \
404                 skipping consistency validation"
405            );
406            Ok(())
407        }
408    }
409}
410
411/// Reject plans that contain write operators when the transaction scope
412/// forbids writes (read-only snapshot, finished scope, etc.).
413///
414/// The sink operator keeps its runtime `check_write_permission` as a backstop;
415/// this check fails the statement right after physical compilation, before any
416/// pipeline stage starts executing.
417fn reject_writes_outside_transaction_scope(
418    plan: &PhysicalPlan,
419    transaction_scope: &TransactionScope,
420) -> DBResult<()> {
421    if !transaction_scope.allows_write() && plan.contains_write_operator() {
422        return Err(DBError::from(QueryError::execution(
423            "write operations are not allowed in the current read-only transaction scope"
424                .to_string(),
425        )));
426    }
427    Ok(())
428}