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 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 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 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 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 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 {
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 context.feedback_history = Some(self.optimizer_engine.feedback_history());
370 context.columnar_policy = Some(self.optimizer_engine.columnar_policy());
374 context
375 }
376}
377
378fn 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
411fn 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}