1use crate::api_core::error::{CoreError, CoreResult};
12use crate::api_core::types::{ExecutionMetadata, QueryRequest, QueryResult};
13use linkrs_metrics::StatsManager;
14use linkrs_query::executor::base::ExecutionResult;
15use linkrs_query::executor::streaming::StreamingQueryResult;
16use linkrs_query::{OptimizerEngine, QueryPipelineManager};
17use linkrs_storage::{
18 AutoCommitBatchOps, AutoCommitGroupOps, QueryStorage, StorageClient, StorageOperationContext,
19};
20use linkrs_transaction::TransactionExecution;
21use parking_lot::RwLock;
22use std::sync::Arc;
23use std::time::Instant;
24
25pub struct QueryApi<S: StorageClient + 'static> {
27 pub(crate) pipeline_manager: QueryPipelineManager<S>,
28}
29
30impl<S: StorageClient + Clone + 'static> QueryApi<S> {
31 pub(crate) fn pipeline_with_engine(
34 storage: Arc<RwLock<S>>,
35 stats_manager: Arc<StatsManager>,
36 optimizer_engine: Arc<OptimizerEngine>,
37 ) -> QueryPipelineManager<S> {
38 QueryPipelineManager::with_optimizer(storage, stats_manager, optimizer_engine)
39 }
40
41 pub fn new(storage: Arc<RwLock<S>>, stats_manager: Arc<StatsManager>) -> Self {
43 let optimizer_engine = Arc::new(OptimizerEngine::default());
44 Self {
45 pipeline_manager: Self::pipeline_with_engine(storage, stats_manager, optimizer_engine),
46 }
47 }
48
49 pub fn execute_with_execution(
55 &mut self,
56 query: &str,
57 ctx: QueryRequest,
58 execution: &TransactionExecution,
59 ) -> CoreResult<QueryResult> {
60 let op_ctx = StorageOperationContext::transaction_with_timestamps(
61 execution.transaction_id(),
62 execution.read_timestamp(),
63 execution.write_timestamp(),
64 execution.read_only(),
65 execution.auto_commit(),
66 );
67 let op_ctx = execution
68 .mutation_recorder()
69 .map_or(op_ctx.clone(), |recorder| {
70 op_ctx.with_mutation_recorder(recorder)
71 });
72 let mut ctx = ctx;
73 ctx.transaction_id = Some(execution.transaction_id());
74 ctx.auto_commit = execution.auto_commit();
75 ctx.isolation_level = Some(execution.isolation_level());
76 self.execute_with_operation_context_and_storage(query, ctx, Some(op_ctx), None)
77 }
78
79 pub fn execute(&mut self, query: &str, ctx: QueryRequest) -> CoreResult<QueryResult> {
88 self.execute_with_operation_context_and_storage(query, ctx, None, None)
89 }
90
91 pub fn execute_with_operation_storage(
92 &mut self,
93 query: &str,
94 ctx: QueryRequest,
95 storage: S,
96 ) -> CoreResult<QueryResult> {
97 let operation_context = storage.operation_context().as_deref().cloned();
98 self.execute_with_operation_context_and_storage(
99 query,
100 ctx,
101 operation_context,
102 Some(storage),
103 )
104 }
105
106 fn execute_with_operation_context_and_storage(
107 &mut self,
108 query: &str,
109 ctx: QueryRequest,
110 operation_context: Option<StorageOperationContext>,
111 operation_storage: Option<S>,
112 ) -> CoreResult<QueryResult> {
113 let start_time = Instant::now();
114 let operation_finalizer = operation_storage.clone();
115
116 let mut request_context = linkrs_query::QueryRequestContext::new(query.to_string());
118 request_context.query_id = ctx.query_id;
119 request_context.transaction_id = ctx.transaction_id.or_else(|| {
120 operation_context
121 .as_ref()
122 .and_then(|context| context.transaction_id)
123 });
124 request_context.auto_commit = ctx.auto_commit;
125 request_context.parameters = ctx.parameters.clone().unwrap_or_default();
126 request_context.session_variables = ctx.session_variables.clone().unwrap_or_default();
127 request_context.parsed_statement = ctx.parsed_statement.clone();
128 request_context.read_only = operation_context
129 .as_ref()
130 .is_some_and(|context| context.read_only);
131 request_context.isolation_level = ctx.isolation_level;
132 request_context.ryw_config = ctx.consistency.read_your_writes().copied();
133 request_context.operation_context = operation_context;
134 request_context.operation_storage = operation_storage
135 .map(|storage| Arc::new(RwLock::new(storage)) as Arc<RwLock<dyn QueryStorage>>);
136 let rctx = Arc::new(request_context);
137
138 let space_info = ctx.space_id.map(|id| {
140 let space_name = ctx.space_name.clone().unwrap_or_default();
141 let mut space_info = linkrs_core::types::SpaceInfo::new(space_name);
142 space_info.space_id = id;
143 space_info
144 });
145
146 let execution_result = match self.pipeline_manager.execute_query_with_request_scope(
148 query,
149 rctx,
150 space_info,
151 ctx.transaction_id,
152 ) {
153 Ok(result) => result,
154 Err(error) => {
155 if ctx.auto_commit {
156 if let Some(storage) = operation_finalizer.as_ref() {
157 storage
158 .finalize_operation(false)
159 .map_err(|cleanup| CoreError::StorageError(cleanup.to_string()))?;
160 }
161 }
162 return Err(CoreError::from(error));
163 }
164 };
165
166 if ctx.auto_commit {
167 if let Some(storage) = operation_finalizer.as_ref() {
168 storage
169 .finalize_operation(true)
170 .map_err(|error| CoreError::StorageError(error.to_string()))?;
171 }
172 }
173
174 let mut result = Self::attach_metadata(execution_result)?;
176 result.metadata.execution_time_ms = start_time.elapsed().as_millis() as u64;
177
178 Ok(result)
179 }
180
181 pub fn execute_stream_with_execution(
183 &mut self,
184 query: &str,
185 ctx: QueryRequest,
186 execution: &TransactionExecution,
187 ) -> CoreResult<StreamingQueryResult> {
188 let op_ctx = StorageOperationContext::transaction_with_timestamps(
189 execution.transaction_id(),
190 execution.read_timestamp(),
191 execution.write_timestamp(),
192 execution.read_only(),
193 execution.auto_commit(),
194 );
195 let op_ctx = execution
196 .mutation_recorder()
197 .map_or(op_ctx.clone(), |recorder| {
198 op_ctx.with_mutation_recorder(recorder)
199 });
200 let mut ctx = ctx;
201 ctx.transaction_id = Some(execution.transaction_id());
202 ctx.auto_commit = execution.auto_commit();
203 ctx.isolation_level = Some(execution.isolation_level());
204 self.execute_stream_with_operation_context_and_storage(query, ctx, Some(op_ctx), None)
205 }
206
207 pub fn execute_stream(
213 &mut self,
214 query: &str,
215 ctx: QueryRequest,
216 ) -> CoreResult<StreamingQueryResult> {
217 self.execute_stream_with_operation_context_and_storage(query, ctx, None, None)
218 }
219
220 pub fn execute_stream_with_operation_storage(
221 &mut self,
222 query: &str,
223 ctx: QueryRequest,
224 storage: S,
225 ) -> CoreResult<StreamingQueryResult> {
226 let operation_context = storage.operation_context().as_deref().cloned();
227 self.execute_stream_with_operation_context_and_storage(
228 query,
229 ctx,
230 operation_context,
231 Some(storage),
232 )
233 }
234
235 fn execute_stream_with_operation_context_and_storage(
236 &mut self,
237 query: &str,
238 ctx: QueryRequest,
239 operation_context: Option<StorageOperationContext>,
240 operation_storage: Option<S>,
241 ) -> CoreResult<StreamingQueryResult> {
242 let operation_owned = operation_storage.is_some() && ctx.auto_commit;
243 let mut request_context = linkrs_query::QueryRequestContext::new(query.to_string());
244 request_context.query_id = ctx.query_id;
245 request_context.transaction_id = ctx.transaction_id.or_else(|| {
246 operation_context
247 .as_ref()
248 .and_then(|context| context.transaction_id)
249 });
250 request_context.auto_commit = ctx.auto_commit;
251 request_context.parameters = ctx.parameters.clone().unwrap_or_default();
252 request_context.session_variables = ctx.session_variables.clone().unwrap_or_default();
253 request_context.parsed_statement = ctx.parsed_statement.clone();
254 request_context.read_only = operation_context
255 .as_ref()
256 .is_some_and(|context| context.read_only);
257 request_context.isolation_level = ctx.isolation_level;
258 request_context.ryw_config = ctx.consistency.read_your_writes().copied();
259 request_context.operation_context = operation_context;
260 request_context.operation_storage = operation_storage
261 .map(|storage| Arc::new(RwLock::new(storage)) as Arc<RwLock<dyn QueryStorage>>);
262 let rctx = Arc::new(request_context);
263
264 let space_info = ctx.space_id.map(|id| {
265 let space_name = ctx.space_name.clone().unwrap_or_default();
266 let mut space_info = linkrs_core::types::SpaceInfo::new(space_name);
267 space_info.space_id = id;
268 space_info
269 });
270
271 let result = self
272 .pipeline_manager
273 .execute_query_stream_with_request_scope(query, rctx, space_info, ctx.transaction_id)
274 .map_err(CoreError::from)?;
275
276 if operation_owned {
277 if let Some(storage) = result.runtime().storage.clone() {
278 let commit_storage = storage.clone();
279 let abort_storage = storage;
280 result.set_transaction_finalizer_with_result(
281 Box::new(move || {
282 commit_storage
283 .write()
284 .finalize_operation(true)
285 .map_err(|error| error.to_string())
286 }),
287 Box::new(move || {
288 abort_storage
289 .write()
290 .finalize_operation(false)
291 .map_err(|error| error.to_string())
292 }),
293 );
294 }
295 }
296 Ok(result)
297 }
298
299 fn attach_metadata(execution: ExecutionResult) -> CoreResult<QueryResult> {
307 let rows_returned = match &execution {
308 ExecutionResult::DataSet { data, .. } => data.row_count(),
309 _ => 0,
310 };
311 match execution {
312 ExecutionResult::Error(msg) => Err(CoreError::Internal(msg)),
313 other => Ok(QueryResult::new(
314 other,
315 ExecutionMetadata {
316 execution_time_ms: 0,
317 rows_scanned: 0,
318 rows_returned: rows_returned as u64,
319 cache_hit: false,
320 },
321 )),
322 }
323 }
324}
325
326impl<S> QueryApi<S>
327where
328 S: StorageClient + Clone + AutoCommitBatchOps + 'static,
329{
330 pub fn execute_batch(
331 &mut self,
332 queries: &[String],
333 ctx: QueryRequest,
334 ) -> Vec<Result<QueryResult, CoreError>> {
335 let mut results = Vec::with_capacity(queries.len());
336 let base_storage = match self.pipeline_manager.storage() {
337 Some(storage) => storage,
338 None => {
339 for _ in queries {
340 results.push(Err(CoreError::Internal("No storage binding".to_string())));
341 }
342 return results;
343 }
344 };
345
346 let window = match base_storage.read().begin_auto_commit_batch() {
347 Ok(window) => window,
348 Err(error) => {
349 for _ in queries {
350 results.push(Err(CoreError::StorageError(error.to_string())));
351 }
352 return results;
353 }
354 };
355
356 for query in queries {
357 let stmt_storage = match base_storage.read().bind_auto_commit_statement(&window) {
358 Ok(storage) => storage,
359 Err(error) => {
360 results.push(Err(CoreError::StorageError(error.to_string())));
361 continue;
362 }
363 };
364 let mut batch_ctx = ctx.clone();
365 batch_ctx.auto_commit = true;
366 batch_ctx.transaction_id = None;
367 results.push(self.execute_with_operation_storage(query, batch_ctx, stmt_storage));
368 }
369
370 if let Err(error) = base_storage.read().finalize_auto_commit_batch(&window) {
371 results.push(Err(CoreError::StorageError(error.to_string())));
372 }
373 results
374 }
375
376 pub fn dml_plan_memo_hits(&self) -> u64 {
377 self.pipeline_manager
378 .last_dml_plan_hits
379 .load(std::sync::atomic::Ordering::Relaxed)
380 }
381
382 pub fn dml_template_ast_parse_count(&self) -> u64 {
383 self.pipeline_manager.dml_template_ast_parse_count()
384 }
385
386 pub fn dml_template_ast_hit_count(&self) -> u64 {
387 self.pipeline_manager.dml_template_ast_hit_count()
388 }
389}
390
391impl<S> QueryApi<S>
392where
393 S: StorageClient + Clone + AutoCommitBatchOps + AutoCommitGroupOps + 'static,
394{
395 pub fn execute_batch_grouped(
403 &mut self,
404 queries: &[String],
405 ctx: QueryRequest,
406 group_size: usize,
407 ) -> Vec<Result<QueryResult, CoreError>> {
408 let group_size = group_size.max(1);
409 let mut results = Vec::with_capacity(queries.len());
410 let base_storage = match self.pipeline_manager.storage() {
411 Some(storage) => storage,
412 None => {
413 for _ in queries {
414 results.push(Err(CoreError::Internal("No storage binding".to_string())));
415 }
416 return results;
417 }
418 };
419
420 for chunk in queries.chunks(group_size) {
421 let window = match base_storage.read().begin_auto_commit_group() {
422 Ok(window) => window,
423 Err(error) => {
424 for _ in chunk {
425 results.push(Err(CoreError::StorageError(error.to_string())));
426 }
427 continue;
428 }
429 };
430
431 for query in chunk {
432 let stmt_storage = match base_storage.read().bind_auto_commit_statement(&window) {
433 Ok(storage) => storage,
434 Err(error) => {
435 results.push(Err(CoreError::StorageError(error.to_string())));
436 continue;
437 }
438 };
439 let mut batch_ctx = ctx.clone();
440 batch_ctx.auto_commit = true;
441 batch_ctx.transaction_id = None;
442 results.push(self.execute_with_operation_storage(query, batch_ctx, stmt_storage));
443 }
444
445 if let Err(error) = base_storage.read().finalize_auto_commit_group(&window) {
446 results.push(Err(CoreError::StorageError(error.to_string())));
447 }
448 }
449 results
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456 use linkrs_storage::MockStorage;
457
458 fn create_mock_storage() -> Arc<RwLock<MockStorage>> {
459 Arc::new(RwLock::new(
460 MockStorage::new().expect("Failed to create MockStorage"),
461 ))
462 }
463
464 #[test]
465 fn statistics_settings_default_to_pipeline_fallbacks() {
466 let api = QueryApi::new(create_mock_storage(), Arc::new(StatsManager::new()));
467 assert_eq!(
468 api.statistics_sample_limit(),
469 linkrs_query::QueryPipelineManager::<MockStorage>::DEFAULT_STATISTICS_SAMPLE_LIMIT
470 );
471 assert_eq!(
472 api.statistics_min_epoch_delta(),
473 linkrs_query::QueryPipelineManager::<MockStorage>::DEFAULT_STATISTICS_MIN_EPOCH_DELTA
474 );
475 }
476
477 #[test]
478 fn statistics_settings_override_reaches_pipeline() {
479 let api = QueryApi::new(create_mock_storage(), Arc::new(StatsManager::new()))
480 .with_statistics_settings(500, 10);
481 assert_eq!(api.statistics_sample_limit(), 500);
482 assert_eq!(api.statistics_min_epoch_delta(), 10);
483 }
484
485 #[test]
486 fn statistics_sample_limit_clamps_to_one() {
487 let api = QueryApi::new(create_mock_storage(), Arc::new(StatsManager::new()))
488 .with_statistics_settings(0, 0);
489 assert_eq!(api.statistics_sample_limit(), 1);
490 assert_eq!(api.statistics_min_epoch_delta(), 0);
491 }
492
493 #[test]
494 fn cost_config_defaults_to_engine_default() {
495 let api = QueryApi::new(create_mock_storage(), Arc::new(StatsManager::new()));
496 assert_eq!(
497 api.cost_config().random_page_cost,
498 linkrs_query::optimizer::CostModelConfig::default().random_page_cost
499 );
500 }
501
502 #[test]
503 fn cost_profile_memory_runtime_reaches_engine() {
504 let api = QueryApi::new(create_mock_storage(), Arc::new(StatsManager::new()))
505 .with_cost_profile(StorageCostProfile::Auto, &RuntimeConfig::memory());
506 assert_eq!(
507 api.cost_config().random_page_cost,
508 linkrs_query::optimizer::CostModelConfig::for_in_memory().random_page_cost
509 );
510 }
511
512 #[test]
513 fn space_overrides_reach_engine_calculators() {
514 use std::collections::HashMap;
515
516 let mut profiles = HashMap::new();
517 profiles.insert("cold".into(), StorageCostProfile::Hdd);
518 let api = QueryApi::new(create_mock_storage(), Arc::new(StatsManager::new()))
519 .with_cost_profile(StorageCostProfile::Ssd, &RuntimeConfig::memory())
520 .with_space_cost_profiles(&profiles, &RuntimeConfig::memory());
521 assert_eq!(
523 api.cost_config().random_page_cost,
524 linkrs_query::optimizer::CostModelConfig::for_in_memory().random_page_cost
525 );
526 let engine = api.pipeline_manager.optimizer_engine();
527 assert_eq!(
528 engine
529 .cost_calculator_for(Some("cold"))
530 .config()
531 .random_page_cost,
532 linkrs_query::optimizer::CostModelConfig::for_in_memory().random_page_cost
533 );
534 }
535
536 #[test]
537 fn cost_config_rebuild_preserves_space_overrides() {
538 use std::collections::HashMap;
539
540 let mut profiles = HashMap::new();
541 profiles.insert("cold".into(), StorageCostProfile::Hdd);
542 let runtime = RuntimeConfig::file("/tmp/linkrs-test-data");
543 let api = QueryApi::new(create_mock_storage(), Arc::new(StatsManager::new()))
544 .with_space_cost_profiles(&profiles, &runtime)
545 .with_cost_config(linkrs_query::optimizer::CostModelConfig::for_ssd());
546 let engine = api.pipeline_manager.optimizer_engine();
547 assert_eq!(
550 engine
551 .cost_calculator_for(Some("cold"))
552 .config()
553 .random_page_cost,
554 linkrs_query::optimizer::CostModelConfig::for_hdd().random_page_cost
555 );
556 assert_eq!(
557 engine
558 .cost_calculator_for(Some("hot"))
559 .config()
560 .random_page_cost,
561 linkrs_query::optimizer::CostModelConfig::for_ssd().random_page_cost
562 );
563 }
564 #[test]
565 fn memory_runtime_short_circuits_explicit_preset() {
566 let api = QueryApi::new(create_mock_storage(), Arc::new(StatsManager::new()))
567 .with_cost_profile(StorageCostProfile::Hdd, &RuntimeConfig::memory());
568 assert_eq!(
569 api.cost_config().random_page_cost,
570 linkrs_query::optimizer::CostModelConfig::for_in_memory().random_page_cost
571 );
572 }
573}