Skip to main content

cqlite_core/query/
executor.rs

1//! Query executor for CQLite
2//!
3//! This module provides query execution capabilities for CQL queries.
4//! It includes:
5//!
6//! - Query plan execution
7//! - Parallel query processing
8//! - Result set construction
9//! - Index utilization
10
11// CQL (Cassandra Query Language) Reference:
12// https://cassandra.apache.org/doc/latest/cassandra/developing/cql/cql_singlefile.html
13//
14// This implements CQL v3.4.3+ for Apache Cassandra 5.0+
15// CQL is NOT SQL - it's a query language specifically designed for Cassandra's distributed architecture.
16
17use super::{
18    planner::{ExecutionStep, IndexSelection, ParallelizationInfo, QueryPlan, StepType},
19    ComparisonOperator, Condition,
20};
21use crate::{
22    schema::SchemaManager, storage::StorageEngine, Config, Error, Result, RowKey, TableId, Value,
23};
24use crossbeam::channel;
25use std::cmp::Ordering;
26use std::collections::HashMap;
27use std::sync::Arc;
28use std::time::Instant;
29
30// Use QueryResult and QueryRow from result module
31pub use super::result::{QueryResult, QueryRow};
32
33/// Default worker count when no parallelization hint is supplied.
34const DEFAULT_PARALLEL_WORKERS: usize = 4;
35
36/// Static fallback used when no plan step requested parallelization. Borrowed to
37/// keep `execute_parallel_table_scan` allocation-free in the hot path.
38static DEFAULT_PARALLELIZATION: ParallelizationInfo = ParallelizationInfo {
39    can_parallelize: true,
40    suggested_threads: DEFAULT_PARALLEL_WORKERS,
41    partition_key: None,
42};
43
44/// Query executor
45#[derive(Debug, Clone)]
46pub struct QueryExecutor {
47    /// Storage engine reference
48    storage: Arc<StorageEngine>,
49    /// Schema manager reference (unused currently but kept for future use)
50    _schema: Arc<SchemaManager>,
51    /// Configuration (kept for future use; surfaced to in-file tests)
52    _config: Config,
53}
54
55impl QueryExecutor {
56    /// Create a new query executor
57    pub fn new(storage: Arc<StorageEngine>, schema: Arc<SchemaManager>, config: &Config) -> Self {
58        Self {
59            storage,
60            _schema: schema,
61            _config: config.clone(),
62        }
63    }
64
65    /// Execute a query plan
66    pub async fn execute(&self, plan: &QueryPlan) -> Result<QueryResult> {
67        let start_time = Instant::now();
68
69        // Classify the plan once so subsequent dispatch is a single match.
70        let has_insert_step = plan
71            .steps
72            .iter()
73            .any(|step| matches!(step.step_type, StepType::Insert));
74        let is_create_table =
75            plan.steps.is_empty() && plan.table.is_some() && plan.estimated_rows == 0;
76
77        #[cfg(debug_assertions)]
78        eprintln!(
79            "DEBUG: Plan steps: {:?}, has_insert_step: {}, is_create_table: {}",
80            plan.steps.iter().map(|s| &s.step_type).collect::<Vec<_>>(),
81            has_insert_step,
82            is_create_table
83        );
84
85        let result = match plan.plan_type {
86            super::planner::PlanType::PointLookup => self.execute_point_lookup(plan).await,
87            super::planner::PlanType::IndexScan => self.execute_index_scan(plan).await,
88            super::planner::PlanType::RangeScan => self.execute_range_scan(plan).await,
89            super::planner::PlanType::TableScan if has_insert_step => {
90                #[cfg(feature = "experimental")]
91                {
92                    self.execute_insert_operation(plan).await
93                }
94                #[cfg(not(feature = "experimental"))]
95                {
96                    Err(Error::UnsupportedFormat(
97                        "INSERT operations require the 'experimental' feature. \
98                         Add 'experimental' to your Cargo.toml features."
99                            .to_string(),
100                    ))
101                }
102            }
103            super::planner::PlanType::TableScan if is_create_table => {
104                self.execute_create_table_operation(plan).await
105            }
106            super::planner::PlanType::TableScan => self.execute_table_scan(plan).await,
107            super::planner::PlanType::Join => self.execute_join(plan).await,
108            super::planner::PlanType::Aggregation => self.execute_aggregation(plan).await,
109            super::planner::PlanType::Subquery => self.execute_subquery(plan).await,
110        };
111
112        let mut query_result = result?;
113        let elapsed_ms = start_time.elapsed().as_millis() as u64;
114
115        #[cfg(debug_assertions)]
116        eprintln!(
117            "DEBUG: Final result before metadata update - rows_affected: {}",
118            query_result.rows_affected
119        );
120
121        query_result.execution_time_ms = elapsed_ms;
122        query_result.metadata.plan_info = Some(super::result::PlanInfo {
123            plan_type: format!("{:?}", plan.plan_type),
124            estimated_cost: plan.estimated_cost,
125            actual_cost: elapsed_ms as f64,
126            // Access path(s) the executor actually consulted (issue #760).
127            indexes_used: Self::indexes_used_for(plan),
128            steps: plan
129                .steps
130                .iter()
131                .map(|s| format!("{:?}", s.step_type))
132                .collect(),
133            parallelization: plan
134                .steps
135                .iter()
136                .find(|s| s.parallelization.can_parallelize)
137                .map(|s| super::result::ParallelizationInfo {
138                    threads_used: s.parallelization.suggested_threads,
139                    effective: true,
140                    partitions: Vec::new(),
141                }),
142        });
143        Ok(query_result)
144    }
145
146    // -- helpers ------------------------------------------------------------
147
148    /// Report the access path(s) the executor *actually consulted* for `plan`,
149    /// for the `indexes_used` field of [`super::result::PlanInfo`] (issue #760,
150    /// Epic #756).
151    ///
152    /// # Truthfulness contract (no-heuristics spirit, issue #28)
153    ///
154    /// This mirrors the dispatch in [`Self::execute`] and reports only what the
155    /// executed code path genuinely does — never what the planner merely
156    /// preferred. The storage layer (`StorageEngine::get` / `scan`) does not yet
157    /// surface *which* on-disk structure (Index.db partition lookup, Summary.db
158    /// sampling, or BTI trie) resolved a partition, so we cannot distinguish
159    /// those sub-paths. We therefore report at the granularity we can prove:
160    ///
161    /// - **Point lookup** (`PointLookup`, and `IndexScan` over a `Primary` or
162    ///   `BloomFilter` index) calls `StorageEngine::get`, which resolves the
163    ///   partition through the partition index. We report the selected index's
164    ///   name (e.g. `"PRIMARY"`).
165    /// - **Sequential scan** (`TableScan`, `RangeScan`, and `IndexScan` over a
166    ///   `Secondary`/`Composite` index — these currently degrade to a full scan
167    ///   in the executor) reports the explicit marker `"scan"`.
168    ///
169    /// ## Scan-marker decision
170    ///
171    /// The issue allows either an empty list or an explicit marker for a full
172    /// scan; we pick the explicit **`"scan"`** marker. An empty list is
173    /// ambiguous (it cannot be told apart from "not yet recorded"), whereas an
174    /// explicit marker makes EXPLAIN-style output and bindings stats truthful
175    /// and self-describing.
176    fn indexes_used_for(plan: &QueryPlan) -> Vec<String> {
177        use super::planner::{IndexType, PlanType, StepType};
178
179        // The marker used for any path that walks rows sequentially.
180        let scan = || vec!["scan".to_string()];
181
182        // A TableScan plan can actually be an INSERT or a CREATE TABLE; those
183        // are dispatched away from `execute_table_scan` in `execute()` and never
184        // call `storage.scan`, so they have no access path to report (roborev
185        // job 40). Mirror that classification here.
186        let has_insert_step = plan
187            .steps
188            .iter()
189            .any(|step| matches!(step.step_type, StepType::Insert));
190        let is_create_table =
191            plan.steps.is_empty() && plan.table.is_some() && plan.estimated_rows == 0;
192        if matches!(plan.plan_type, PlanType::TableScan) && (has_insert_step || is_create_table) {
193            return Vec::new();
194        }
195
196        match plan.plan_type {
197            // Resolves a single partition via `StorageEngine::get`.
198            PlanType::PointLookup => match plan.selected_indexes.first() {
199                Some(idx) => vec![idx.index_name.clone()],
200                // No selected index recorded but we still did a partition
201                // lookup — report the generic primary-key path.
202                None => vec!["PRIMARY".to_string()],
203            },
204            // IndexScan dispatch depends on the index type: Primary/Bloom do a
205            // real point lookup; Secondary/Composite degrade to a full scan.
206            PlanType::IndexScan => match plan.selected_indexes.first() {
207                Some(idx) => match idx.index_type {
208                    IndexType::Primary | IndexType::BloomFilter => {
209                        vec![idx.index_name.clone()]
210                    }
211                    IndexType::Secondary | IndexType::Composite => scan(),
212                },
213                None => scan(),
214            },
215            // Sequential-scan paths.
216            PlanType::TableScan | PlanType::RangeScan => scan(),
217            // Placeholder executors return empty results without touching any
218            // index structure; report nothing rather than fabricate a path.
219            PlanType::Join | PlanType::Aggregation | PlanType::Subquery => Vec::new(),
220        }
221    }
222
223    /// Resolve `plan.table` or surface a uniform query-execution error.
224    fn require_table<'a>(&self, plan: &'a QueryPlan) -> Result<&'a TableId> {
225        plan.table
226            .as_ref()
227            .ok_or_else(|| Error::query_execution("Missing table in plan"))
228    }
229
230    /// Find the first condition matching `column` across all steps.
231    fn find_condition<'a>(steps: &'a [ExecutionStep], column: &str) -> Option<&'a Condition> {
232        steps
233            .iter()
234            .flat_map(|s| s.conditions.iter())
235            .find(|c| c.column == column)
236    }
237
238    /// Convert a `(key, data)` pair from `StorageEngine::scan` into rows.
239    fn scan_pairs_to_rows(&self, pairs: Vec<(RowKey, Value)>) -> Result<Vec<QueryRow>> {
240        let mut rows = Vec::with_capacity(pairs.len());
241        for (row_key, row_data) in pairs {
242            rows.push(self.storage_data_to_query_row(row_data, &row_key)?);
243        }
244        Ok(rows)
245    }
246
247    /// Run a full table scan and materialize results.
248    async fn full_scan_rows(&self, table: &TableId) -> Result<Vec<QueryRow>> {
249        let scan_results = self.storage.scan(table, None, None, None, None).await?;
250        self.scan_pairs_to_rows(scan_results)
251    }
252
253    /// Look up a single row by the key derived from `condition`.
254    async fn point_lookup_rows(
255        &self,
256        table: &TableId,
257        condition: &Condition,
258    ) -> Result<Vec<QueryRow>> {
259        let row_key = self.condition_to_row_key(condition)?;
260        match self.storage.get(table, &row_key).await? {
261            Some(row_data) => Ok(vec![self.storage_data_to_query_row(row_data, &row_key)?]),
262            None => Ok(Vec::new()),
263        }
264    }
265
266    /// Wrap a row collection in a `QueryResult`. `execution_time_ms` is set by `execute()`.
267    fn make_result(rows: Vec<QueryRow>) -> QueryResult {
268        QueryResult::with_rows(rows)
269    }
270
271    // -- plan executors -----------------------------------------------------
272
273    /// Execute point lookup plan
274    async fn execute_point_lookup(&self, plan: &QueryPlan) -> Result<QueryResult> {
275        let table = self.require_table(plan)?;
276
277        // Find the lookup condition (first condition of the first step that has any).
278        let lookup_condition = plan
279            .steps
280            .iter()
281            .find_map(|step| step.conditions.first())
282            .ok_or_else(|| Error::query_execution("No lookup condition found"))?;
283
284        let row_key = self.condition_to_row_key(lookup_condition)?;
285
286        #[cfg(debug_assertions)]
287        eprintln!(
288            "DEBUG: SELECT point lookup using row key: {:?}",
289            std::str::from_utf8(row_key.as_bytes()).unwrap_or("<invalid-utf8>")
290        );
291
292        let mut rows = Vec::new();
293        if let Some(row_data) = self.storage.get(table, &row_key).await? {
294            rows.push(self.storage_data_to_query_row(row_data, &row_key)?);
295        }
296
297        Ok(Self::make_result(rows))
298    }
299
300    /// Execute index scan plan
301    async fn execute_index_scan(&self, plan: &QueryPlan) -> Result<QueryResult> {
302        let table = self.require_table(plan)?;
303
304        let index_selection = plan
305            .selected_indexes
306            .first()
307            .ok_or_else(|| Error::query_execution("No index selected"))?;
308
309        let mut rows = match index_selection.index_type {
310            super::planner::IndexType::Secondary => {
311                self.execute_secondary_index_scan(table, index_selection, &plan.steps)
312                    .await?
313            }
314            super::planner::IndexType::BloomFilter => {
315                self.execute_bloom_filter_scan(table, index_selection, &plan.steps)
316                    .await?
317            }
318            super::planner::IndexType::Primary => {
319                self.execute_primary_index_scan(table, index_selection, &plan.steps)
320                    .await?
321            }
322            super::planner::IndexType::Composite => {
323                self.execute_composite_index_scan(table, index_selection, &plan.steps)
324                    .await?
325            }
326        };
327
328        rows = self.apply_execution_steps(rows, &plan.steps).await?;
329        Ok(Self::make_result(rows))
330    }
331
332    /// Execute range scan plan
333    async fn execute_range_scan(&self, plan: &QueryPlan) -> Result<QueryResult> {
334        let table = self.require_table(plan)?;
335
336        // Range conditions are recognized by the planner; the storage engine is
337        // queried with no explicit bounds for now.
338        let mut rows = self.full_scan_rows(table).await?;
339        rows = self.apply_execution_steps(rows, &plan.steps).await?;
340        Ok(Self::make_result(rows))
341    }
342
343    /// Execute table scan plan
344    async fn execute_table_scan(&self, plan: &QueryPlan) -> Result<QueryResult> {
345        let table = self.require_table(plan)?;
346
347        #[cfg(debug_assertions)]
348        log::debug!("executor: Scanning for table: {:?}", table.name());
349
350        let can_parallelize = plan
351            .steps
352            .iter()
353            .any(|step| step.parallelization.can_parallelize);
354
355        let mut rows = if can_parallelize {
356            self.execute_parallel_table_scan(table, &plan.steps).await?
357        } else {
358            self.full_scan_rows(table).await?
359        };
360
361        rows = self.apply_execution_steps(rows, &plan.steps).await?;
362        Ok(Self::make_result(rows))
363    }
364
365    /// Execute join plan (placeholder)
366    async fn execute_join(&self, _plan: &QueryPlan) -> Result<QueryResult> {
367        Ok(QueryResult::new())
368    }
369
370    /// Execute aggregation plan (placeholder)
371    async fn execute_aggregation(&self, _plan: &QueryPlan) -> Result<QueryResult> {
372        Ok(QueryResult::new())
373    }
374
375    /// Execute subquery plan (placeholder)
376    async fn execute_subquery(&self, _plan: &QueryPlan) -> Result<QueryResult> {
377        Ok(QueryResult::new())
378    }
379
380    // -- index scans --------------------------------------------------------
381
382    /// Execute secondary index scan (currently a full scan; secondary index
383    /// support is tracked separately).
384    async fn execute_secondary_index_scan(
385        &self,
386        table: &TableId,
387        index_selection: &IndexSelection,
388        steps: &[ExecutionStep],
389    ) -> Result<Vec<QueryRow>> {
390        // Validate the index condition exists; the lookup itself is not yet wired up.
391        Self::find_condition(steps, &index_selection.columns[0])
392            .ok_or_else(|| Error::query_execution("No condition found for index"))?;
393        self.full_scan_rows(table).await
394    }
395
396    /// Execute bloom filter scan (degrades to a direct point lookup).
397    async fn execute_bloom_filter_scan(
398        &self,
399        table: &TableId,
400        index_selection: &IndexSelection,
401        steps: &[ExecutionStep],
402    ) -> Result<Vec<QueryRow>> {
403        let condition = Self::find_condition(steps, &index_selection.columns[0])
404            .ok_or_else(|| Error::query_execution("No condition found for bloom filter"))?;
405        self.point_lookup_rows(table, condition).await
406    }
407
408    /// Execute primary index scan (point lookup on the primary key).
409    async fn execute_primary_index_scan(
410        &self,
411        table: &TableId,
412        index_selection: &IndexSelection,
413        steps: &[ExecutionStep],
414    ) -> Result<Vec<QueryRow>> {
415        let condition = Self::find_condition(steps, &index_selection.columns[0])
416            .ok_or_else(|| Error::query_execution("No condition found for primary key"))?;
417        self.point_lookup_rows(table, condition).await
418    }
419
420    /// Execute composite index scan (currently a full scan; composite lookups
421    /// are tracked separately).
422    async fn execute_composite_index_scan(
423        &self,
424        table: &TableId,
425        _index_selection: &IndexSelection,
426        _steps: &[ExecutionStep],
427    ) -> Result<Vec<QueryRow>> {
428        self.full_scan_rows(table).await
429    }
430
431    // -- table scans --------------------------------------------------------
432
433    /// Execute parallel table scan.
434    ///
435    /// NOTE: All workers currently issue the same `storage.scan(...)` and the
436    /// receiver deduplicates implicitly by virtue of preserving emission order;
437    /// this matches the behavior present before refactoring. A real parallel
438    /// scan would partition the key range across workers.
439    async fn execute_parallel_table_scan(
440        &self,
441        table: &TableId,
442        steps: &[ExecutionStep],
443    ) -> Result<Vec<QueryRow>> {
444        let parallelization = steps
445            .iter()
446            .find(|step| step.parallelization.can_parallelize)
447            .map(|step| &step.parallelization)
448            .unwrap_or(&DEFAULT_PARALLELIZATION);
449
450        let thread_count = parallelization.suggested_threads;
451        let (tx, rx) = channel::unbounded();
452
453        let mut handles = Vec::with_capacity(thread_count);
454        for worker_id in 0..thread_count {
455            let storage = self.storage.clone();
456            let table = table.clone();
457            let tx = tx.clone();
458
459            handles.push(tokio::spawn(async move {
460                match storage.scan(&table, None, None, None, None).await {
461                    Ok(results) => {
462                        for pair in results {
463                            // Receiver hung up — bail out early.
464                            if tx.send(pair).is_err() {
465                                break;
466                            }
467                        }
468                    }
469                    Err(e) => log::error!("Worker {} error: {:?}", worker_id, e),
470                }
471            }));
472        }
473
474        // Drop our local sender so `rx` closes once the workers finish.
475        drop(tx);
476
477        let mut rows = Vec::new();
478        while let Ok((row_key, row_data)) = rx.recv() {
479            rows.push(self.storage_data_to_query_row(row_data, &row_key)?);
480        }
481
482        for handle in handles {
483            let _ = handle.await;
484        }
485
486        Ok(rows)
487    }
488
489    // -- execution-step pipeline -------------------------------------------
490
491    /// Apply execution steps to result rows.
492    ///
493    /// Limit/Aggregate/Join/Insert/Scan are no-ops at this layer (handled
494    /// elsewhere or not yet implemented); only Filter/Sort/Project transform
495    /// the row stream.
496    async fn apply_execution_steps(
497        &self,
498        mut rows: Vec<QueryRow>,
499        steps: &[ExecutionStep],
500    ) -> Result<Vec<QueryRow>> {
501        for step in steps {
502            match step.step_type {
503                StepType::Filter => rows = self.apply_filter_step(rows, step)?,
504                StepType::Sort => rows = self.apply_sort_step(rows, step),
505                StepType::Project => rows = self.apply_project_step(rows, step),
506                // Limit is enforced higher up; the rest are placeholders.
507                StepType::Limit
508                | StepType::Aggregate
509                | StepType::Join
510                | StepType::Scan
511                | StepType::Insert => {}
512            }
513        }
514        Ok(rows)
515    }
516
517    /// Apply filter step
518    fn apply_filter_step(
519        &self,
520        rows: Vec<QueryRow>,
521        step: &ExecutionStep,
522    ) -> Result<Vec<QueryRow>> {
523        let mut filtered_rows = Vec::with_capacity(rows.len());
524        for row in rows {
525            let mut matches = true;
526            for condition in &step.conditions {
527                if !self.evaluate_condition(&row, condition)? {
528                    matches = false;
529                    break;
530                }
531            }
532            if matches {
533                filtered_rows.push(row);
534            }
535        }
536        Ok(filtered_rows)
537    }
538
539    /// Apply sort step
540    fn apply_sort_step(&self, mut rows: Vec<QueryRow>, step: &ExecutionStep) -> Vec<QueryRow> {
541        let Some(sort_column) = step.columns.first() else {
542            return rows;
543        };
544
545        rows.sort_by(|a, b| {
546            let a_val = a.values.get(sort_column).unwrap_or(&Value::Null);
547            let b_val = b.values.get(sort_column).unwrap_or(&Value::Null);
548            self.compare_values(a_val, b_val).unwrap_or(Ordering::Equal)
549        });
550        rows
551    }
552
553    /// Apply project step
554    fn apply_project_step(&self, rows: Vec<QueryRow>, step: &ExecutionStep) -> Vec<QueryRow> {
555        rows.into_iter()
556            .map(|row| {
557                let mut projected_values = HashMap::with_capacity(step.columns.len());
558                for column in &step.columns {
559                    if let Some(value) = row.values.get(column) {
560                        projected_values.insert(column.clone(), value.clone());
561                    }
562                }
563                QueryRow::with_values(row.key, projected_values)
564            })
565            .collect()
566    }
567
568    // -- condition / value helpers -----------------------------------------
569
570    /// Evaluate a condition against a row
571    fn evaluate_condition(&self, row: &QueryRow, condition: &Condition) -> Result<bool> {
572        let row_value = row.values.get(&condition.column).unwrap_or(&Value::Null);
573
574        match condition.operator {
575            ComparisonOperator::Equal => Ok(row_value == &condition.value),
576            ComparisonOperator::NotEqual => Ok(row_value != &condition.value),
577            ComparisonOperator::LessThan => Ok(matches!(
578                self.compare_values(row_value, &condition.value)?,
579                Ordering::Less
580            )),
581            ComparisonOperator::LessThanOrEqual => Ok(matches!(
582                self.compare_values(row_value, &condition.value)?,
583                Ordering::Less | Ordering::Equal
584            )),
585            ComparisonOperator::GreaterThan => Ok(matches!(
586                self.compare_values(row_value, &condition.value)?,
587                Ordering::Greater
588            )),
589            ComparisonOperator::GreaterThanOrEqual => Ok(matches!(
590                self.compare_values(row_value, &condition.value)?,
591                Ordering::Greater | Ordering::Equal
592            )),
593            // Simplified IN / NOT IN: treat as equality / inequality for now.
594            ComparisonOperator::In => Ok(row_value == &condition.value),
595            ComparisonOperator::NotIn => Ok(row_value != &condition.value),
596            ComparisonOperator::Like => match (row_value, &condition.value) {
597                (Value::Text(row_text), Value::Text(pattern)) => Ok(row_text.contains(pattern)),
598                _ => Ok(false),
599            },
600            ComparisonOperator::NotLike => match (row_value, &condition.value) {
601                (Value::Text(row_text), Value::Text(pattern)) => Ok(!row_text.contains(pattern)),
602                _ => Ok(true),
603            },
604        }
605    }
606
607    /// Compare two values
608    fn compare_values(&self, a: &Value, b: &Value) -> Result<Ordering> {
609        match (a, b) {
610            (Value::Integer(a), Value::Integer(b)) => Ok(a.cmp(b)),
611            (Value::Float(a), Value::Float(b)) => Ok(a.partial_cmp(b).unwrap_or(Ordering::Equal)),
612            (Value::Text(a), Value::Text(b)) => Ok(a.cmp(b)),
613            (Value::Boolean(a), Value::Boolean(b)) => Ok(a.cmp(b)),
614            // UUID comparison: byte-wise (same as Cassandra's ordering).
615            // Covers both UUID and TIMEUUID columns — both are stored as Value::Uuid.
616            (Value::Uuid(a), Value::Uuid(b)) => Ok(a.cmp(b)),
617            (Value::Null, Value::Null) => Ok(Ordering::Equal),
618            (Value::Null, _) => Ok(Ordering::Less),
619            (_, Value::Null) => Ok(Ordering::Greater),
620            _ => Err(Error::query_execution(
621                "Cannot compare values of different types",
622            )),
623        }
624    }
625
626    /// Convert a [`Value`] to the raw partition-key bytes used by [`RowKey`] and
627    /// the Index.db lookup table.
628    ///
629    /// The encoding follows the same contract as
630    /// [`PartitionKey::to_bytes`](crate::storage::write_engine::mutation::PartitionKey::to_bytes):
631    ///
632    /// - **Single-component keys** — raw value bytes (UUID = 16 bytes, Int = 4 BE
633    ///   bytes, Text = UTF-8, BigInt = 8 BE bytes, …).
634    /// - **Multi-component (composite) keys** — `[len: u16 BE][value bytes][0x00]`
635    ///   per component, including a trailing `0x00` after the final component.
636    ///   Pass a `Value::Tuple` whose elements are the ordered PK components.
637    fn value_to_row_key(&self, value: &Value) -> Result<RowKey> {
638        match value {
639            Value::Integer(i) => Ok(RowKey::new(i.to_be_bytes().to_vec())),
640            Value::Text(s) => Ok(RowKey::new(s.as_bytes().to_vec())),
641            Value::Float(f) => Ok(RowKey::new(f.to_be_bytes().to_vec())),
642            Value::Boolean(b) => Ok(RowKey::new(vec![u8::from(*b)])),
643            Value::Null => Ok(RowKey::new(vec![0])),
644            // UUID and TIMEUUID are both stored as 16 raw bytes (no framing).
645            // This matches PartitionKey::to_bytes single-component output for a UUID column.
646            Value::Uuid(bytes) => Ok(RowKey::new(bytes.to_vec())),
647            Value::BigInt(i) => Ok(RowKey::new(i.to_be_bytes().to_vec())),
648            // Multi-component (composite) partition key passed as a Tuple.
649            // Encoding: [len: u16 BE][value bytes][0x00] per component, identical to
650            // PartitionKey::to_bytes multi-component output (see mutation.rs ~line 256).
651            Value::Tuple(components) => {
652                let mut result = Vec::new();
653                for component in components {
654                    let raw = self.value_to_raw_pk_bytes(component)?;
655                    let len = raw.len();
656                    if len > u16::MAX as usize {
657                        return Err(Error::query_execution(
658                            "Composite partition key component too large",
659                        ));
660                    }
661                    result.extend_from_slice(&(len as u16).to_be_bytes());
662                    result.extend_from_slice(&raw);
663                    result.push(0x00);
664                }
665                Ok(RowKey::new(result))
666            }
667            _ => Err(Error::query_execution("Cannot convert value to row key")),
668        }
669    }
670
671    /// Serialize a single value to raw bytes suitable for inclusion in a
672    /// composite partition key component. Used by [`value_to_row_key`] for
673    /// `Value::Tuple` components.
674    fn value_to_raw_pk_bytes(&self, value: &Value) -> Result<Vec<u8>> {
675        match value {
676            Value::Integer(i) => Ok(i.to_be_bytes().to_vec()),
677            Value::Text(s) => Ok(s.as_bytes().to_vec()),
678            Value::Float(f) => Ok(f.to_be_bytes().to_vec()),
679            Value::Boolean(b) => Ok(vec![u8::from(*b)]),
680            Value::Null => Ok(Vec::new()),
681            Value::Uuid(bytes) => Ok(bytes.to_vec()),
682            Value::BigInt(i) => Ok(i.to_be_bytes().to_vec()),
683            _ => Err(Error::query_execution(
684                "Cannot serialize value as partition key component",
685            )),
686        }
687    }
688
689    /// Convert Condition to RowKey (consistent with INSERT)
690    fn condition_to_row_key(&self, condition: &Condition) -> Result<RowKey> {
691        // Match the key format used by INSERT for "id" columns.
692        if condition.column == "id" {
693            if let Value::Integer(id) = &condition.value {
694                return Ok(RowKey::new(format!("user_key_{}", id).into_bytes()));
695            }
696        }
697        self.value_to_row_key(&condition.value)
698    }
699
700    /// Convert storage data to query row
701    fn storage_data_to_query_row(&self, data: Value, key: &RowKey) -> Result<QueryRow> {
702        let mut values = HashMap::new();
703
704        // Storage path stores rows as `Value::Map` keyed by column name (Text).
705        match data {
706            Value::Map(map) => {
707                for (map_key, map_value) in map {
708                    if let Value::Text(column_name) = map_key {
709                        values.insert(column_name, map_value);
710                    }
711                }
712            }
713            other => {
714                values.insert("data".to_string(), other);
715            }
716        }
717
718        // If no values were extracted, surface the row key for visibility.
719        if values.is_empty() {
720            values.insert("id".to_string(), Value::Text(format!("{:?}", key)));
721        }
722
723        Ok(QueryRow::with_values(key.clone(), values))
724    }
725
726    // -- experimental write paths ------------------------------------------
727
728    /// Execute INSERT operation
729    #[cfg(feature = "experimental")]
730    async fn execute_insert_operation(&self, plan: &QueryPlan) -> Result<QueryResult> {
731        let table_id = self
732            .require_table(plan)
733            .map_err(|_| Error::query_execution("No table specified in INSERT plan"))?;
734
735        let mut inserted_count: u64 = 0;
736
737        for step in &plan.steps {
738            if !matches!(step.step_type, StepType::Insert) {
739                continue;
740            }
741
742            #[cfg(debug_assertions)]
743            eprintln!("DEBUG: INSERT step conditions: {:?}", step.conditions);
744
745            // Default key uses the running insert index; an explicit "id"
746            // condition wins so SELECT and INSERT share the same key shape.
747            let mut key_value = format!("test_key_{}", inserted_count);
748            for condition in &step.conditions {
749                if condition.column == "id" {
750                    if let Value::Integer(id) = &condition.value {
751                        key_value = format!("user_key_{}", id);
752                        break;
753                    }
754                }
755            }
756
757            #[cfg(debug_assertions)]
758            eprintln!("DEBUG: Using row key: {}", key_value);
759
760            let row_key = RowKey::new(key_value.into_bytes());
761
762            // Build the row payload from step conditions (or seed defaults
763            // when the step carries none, for test compatibility).
764            let mut value_map: HashMap<String, Value> = step
765                .conditions
766                .iter()
767                .map(|c| (c.column.clone(), c.value.clone()))
768                .collect();
769
770            if value_map.is_empty() {
771                value_map.insert("id".to_string(), Value::Integer(inserted_count as i32 + 1));
772                value_map.insert(
773                    "name".to_string(),
774                    Value::Text(format!("TestUser{}", inserted_count + 1)),
775                );
776            }
777
778            let row_value = map_to_value(value_map);
779
780            self.storage.put(table_id, row_key, row_value).await?;
781            inserted_count += 1;
782
783            #[cfg(debug_assertions)]
784            eprintln!(
785                "DEBUG: execute_insert_operation - stored row {} in table {}",
786                inserted_count, table_id
787            );
788        }
789
790        // No explicit INSERT steps — emit a single placeholder row to keep
791        // legacy tests passing.
792        if inserted_count == 0 {
793            let row_key = RowKey::new(b"default_test_key".to_vec());
794            let mut value_map = HashMap::new();
795            value_map.insert("id".to_string(), Value::Integer(1));
796            value_map.insert("name".to_string(), Value::Text("DefaultUser".to_string()));
797
798            self.storage
799                .put(table_id, row_key, map_to_value(value_map))
800                .await?;
801            inserted_count = 1;
802        }
803
804        #[cfg(debug_assertions)]
805        eprintln!(
806            "DEBUG: execute_insert_operation called, returning rows_affected: {}",
807            inserted_count
808        );
809
810        Ok(QueryResult {
811            rows: vec![],
812            rows_affected: inserted_count,
813            execution_time_ms: 0,
814            metadata: super::result::QueryMetadata::default(),
815        })
816    }
817
818    /// Execute CREATE TABLE operation (placeholder — DDL isn't persisted yet).
819    async fn execute_create_table_operation(&self, _plan: &QueryPlan) -> Result<QueryResult> {
820        Ok(QueryResult {
821            rows: vec![],
822            rows_affected: 0,
823            execution_time_ms: 0,
824            metadata: super::result::QueryMetadata::default(),
825        })
826    }
827}
828
829/// Build a `Value::Map` from a string-keyed map for storage writes.
830#[cfg(feature = "experimental")]
831fn map_to_value(map: HashMap<String, Value>) -> Value {
832    Value::Map(map.into_iter().map(|(k, v)| (Value::Text(k), v)).collect())
833}
834
835#[cfg(test)]
836mod tests {
837    use super::*;
838    use crate::Config;
839    use std::sync::Arc;
840    use tempfile::TempDir;
841
842    /// Construct a fresh executor against a temporary storage root.
843    async fn make_executor() -> (TempDir, QueryExecutor, Config) {
844        let temp_dir = TempDir::new().unwrap();
845        let config = Config::default();
846        let platform = Arc::new(crate::platform::Platform::new(&config).await.unwrap());
847        let storage = Arc::new(
848            crate::storage::StorageEngine::open(
849                temp_dir.path(),
850                &config,
851                platform,
852                #[cfg(feature = "state_machine")]
853                None,
854            )
855            .await
856            .unwrap(),
857        );
858        let schema = Arc::new(
859            crate::schema::SchemaManager::new(temp_dir.path())
860                .await
861                .unwrap(),
862        );
863        let executor = QueryExecutor::new(storage, schema, &config);
864        (temp_dir, executor, config)
865    }
866
867    #[tokio::test]
868    async fn test_query_executor_creation() {
869        let (_tmp, executor, config) = make_executor().await;
870        assert_eq!(
871            executor._config.query.query_parallelism,
872            config.query.query_parallelism
873        );
874    }
875
876    #[tokio::test]
877    async fn test_value_comparison() {
878        let (_tmp, executor, _) = make_executor().await;
879
880        let result = executor
881            .compare_values(&Value::Integer(10), &Value::Integer(20))
882            .unwrap();
883        assert_eq!(result, Ordering::Less);
884
885        let result = executor
886            .compare_values(
887                &Value::Text("apple".to_string()),
888                &Value::Text("banana".to_string()),
889            )
890            .unwrap();
891        assert_eq!(result, Ordering::Less);
892    }
893
894    #[tokio::test]
895    async fn test_condition_evaluation() {
896        let (_tmp, executor, _) = make_executor().await;
897
898        let mut row_values = HashMap::new();
899        row_values.insert("id".to_string(), Value::Integer(1));
900        row_values.insert("name".to_string(), Value::Text("test".to_string()));
901        let row = QueryRow::with_values(RowKey::new(vec![1]), row_values);
902
903        let condition = Condition {
904            column: "id".to_string(),
905            operator: ComparisonOperator::Equal,
906            value: Value::Integer(1),
907        };
908        assert!(executor.evaluate_condition(&row, &condition).unwrap());
909
910        let condition = Condition {
911            column: "name".to_string(),
912            operator: ComparisonOperator::Like,
913            value: Value::Text("test".to_string()),
914        };
915        assert!(executor.evaluate_condition(&row, &condition).unwrap());
916    }
917
918    // -- indexes_used access-path reporting (issue #760, Epic #756) --------
919
920    use super::super::planner::{IndexSelection, IndexType};
921
922    /// Build a minimal plan with the given type and selected indexes.
923    fn plan_with(
924        plan_type: super::super::planner::PlanType,
925        selected_indexes: Vec<IndexSelection>,
926    ) -> QueryPlan {
927        QueryPlan {
928            plan_type,
929            table: None,
930            estimated_cost: 0.0,
931            estimated_rows: 0,
932            selected_indexes,
933            steps: Vec::new(),
934            hints: super::super::planner::QueryHints::default(),
935        }
936    }
937
938    fn primary_index() -> IndexSelection {
939        IndexSelection {
940            index_name: "PRIMARY".to_string(),
941            columns: vec!["id".to_string()],
942            selectivity: 0.1,
943            index_type: IndexType::Primary,
944        }
945    }
946
947    /// A point lookup resolves the partition via the partition index
948    /// (Index.db / Summary.db) — it MUST report the index it used, not "scan".
949    #[test]
950    fn test_indexes_used_point_lookup_reports_partition_index() {
951        let plan = plan_with(
952            super::super::planner::PlanType::PointLookup,
953            vec![primary_index()],
954        );
955        assert_eq!(QueryExecutor::indexes_used_for(&plan), vec!["PRIMARY"]);
956    }
957
958    /// A full table scan reports the explicit "scan" marker (we picked the
959    /// marker over an empty list so EXPLAIN output is unambiguous).
960    #[test]
961    fn test_indexes_used_table_scan_reports_scan_marker() {
962        let plan = plan_with(super::super::planner::PlanType::TableScan, Vec::new());
963        assert_eq!(QueryExecutor::indexes_used_for(&plan), vec!["scan"]);
964    }
965
966    /// Regression (roborev job 40): a TableScan plan that is actually an INSERT
967    /// or a CREATE TABLE never calls `storage.scan`, so it must NOT report the
968    /// "scan" access path. `execute()` special-cases these before
969    /// `execute_table_scan`; `indexes_used_for` must mirror that.
970    #[test]
971    fn test_indexes_used_insert_and_ddl_table_scan_report_no_scan() {
972        use super::super::planner::{ParallelizationInfo, StepType};
973
974        // INSERT: a TableScan plan carrying an Insert step.
975        let insert_step = ExecutionStep {
976            step_type: StepType::Insert,
977            columns: Vec::new(),
978            conditions: Vec::new(),
979            cost: 0.0,
980            parallelization: ParallelizationInfo {
981                can_parallelize: false,
982                suggested_threads: 1,
983                partition_key: None,
984            },
985        };
986        let mut insert_plan = plan_with(super::super::planner::PlanType::TableScan, Vec::new());
987        insert_plan.steps = vec![insert_step];
988        assert!(
989            QueryExecutor::indexes_used_for(&insert_plan).is_empty(),
990            "INSERT must not report a scan access path"
991        );
992
993        // CREATE TABLE: empty steps, a target table, zero estimated rows.
994        let mut ddl_plan = plan_with(super::super::planner::PlanType::TableScan, Vec::new());
995        ddl_plan.table = Some(TableId::new("t"));
996        ddl_plan.estimated_rows = 0;
997        assert!(
998            QueryExecutor::indexes_used_for(&ddl_plan).is_empty(),
999            "CREATE TABLE must not report a scan access path"
1000        );
1001    }
1002
1003    /// Range scans degrade to a sequential scan in the executor → "scan".
1004    #[test]
1005    fn test_indexes_used_range_scan_reports_scan_marker() {
1006        let plan = plan_with(super::super::planner::PlanType::RangeScan, Vec::new());
1007        assert_eq!(QueryExecutor::indexes_used_for(&plan), vec!["scan"]);
1008    }
1009
1010    /// IndexScan on a Primary/Bloom index does a real point lookup → report
1011    /// the index name. (These executor paths call `storage.get`.)
1012    #[test]
1013    fn test_indexes_used_index_scan_primary_reports_index() {
1014        let plan = plan_with(
1015            super::super::planner::PlanType::IndexScan,
1016            vec![primary_index()],
1017        );
1018        assert_eq!(QueryExecutor::indexes_used_for(&plan), vec!["PRIMARY"]);
1019    }
1020
1021    /// IndexScan on a Secondary index currently degrades to a full scan in the
1022    /// executor (the secondary lookup is not yet wired up). Report "scan" —
1023    /// reporting the index would be fabrication.
1024    #[test]
1025    fn test_indexes_used_index_scan_secondary_reports_scan() {
1026        let secondary = IndexSelection {
1027            index_name: "idx_name".to_string(),
1028            columns: vec!["name".to_string()],
1029            selectivity: 0.1,
1030            index_type: IndexType::Secondary,
1031        };
1032        let plan = plan_with(super::super::planner::PlanType::IndexScan, vec![secondary]);
1033        assert_eq!(QueryExecutor::indexes_used_for(&plan), vec!["scan"]);
1034    }
1035
1036    #[tokio::test]
1037    async fn test_condition_to_row_key_mapping() {
1038        let (_tmp, executor, _) = make_executor().await;
1039
1040        let id_condition = Condition {
1041            column: "id".to_string(),
1042            operator: ComparisonOperator::Equal,
1043            value: Value::Integer(42),
1044        };
1045        let key = executor
1046            .condition_to_row_key(&id_condition)
1047            .expect("id condition key");
1048        assert_eq!(std::str::from_utf8(key.as_bytes()).unwrap(), "user_key_42");
1049
1050        let name_condition = Condition {
1051            column: "username".to_string(),
1052            operator: ComparisonOperator::Equal,
1053            value: Value::Text("carol".to_string()),
1054        };
1055        let key = executor
1056            .condition_to_row_key(&name_condition)
1057            .expect("fallback key");
1058        assert_eq!(key.as_bytes(), b"carol");
1059    }
1060}