Skip to main content

cqlite_core/query/
result.rs

1//! Query result types for CQLite
2//!
3//! This module provides result types and utilities for query execution results.
4//! It includes result set management, row iteration, and result metadata.
5
6use crate::{schema::CqlType, RowKey, Value};
7// Re-export cell metadata types that now live in crate::types so the storage
8// layer can use them without a cyclic dependency.
9pub use crate::types::{CellExpiration, CellWriteMetadata};
10use base64::Engine;
11use serde::{Deserialize, Serialize};
12use serde_json::json;
13use std::collections::HashMap;
14use std::fmt;
15use std::sync::Arc;
16use tokio::sync::mpsc;
17
18/// Encode bytes as standard base64 (used across JSON serializers below).
19fn b64(bytes: &[u8]) -> String {
20    base64::engine::general_purpose::STANDARD.encode(bytes)
21}
22
23/// True if `RowMetadata` carries anything worth serializing.
24fn row_metadata_is_populated(meta: &RowMetadata) -> bool {
25    meta.version.is_some() || meta.ttl.is_some() || !meta.tags.is_empty()
26}
27
28// ============================================================================
29// Per-cell write metadata (Issue #691)
30// ============================================================================
31//
32// CellWriteMetadata and CellExpiration are defined in crate::types and
33// re-exported above.  All usages of these types within this module
34// and its callers are unchanged — the re-export makes the move transparent.
35
36/// Projection-level flags that control opt-in metadata collection.
37///
38/// Created during query planning and threaded to the scan/build path.
39/// When all flags are `false` (the default), the hot path allocates nothing
40/// extra — `QueryRow::cell_metadata` stays `None`.
41#[derive(Debug, Clone, Default)]
42pub struct ProjectionFlags {
43    /// Set when the SELECT list contains at least one `WRITETIME(col)` or
44    /// `TTL(col)` expression.  Causes per-cell metadata to be attached to
45    /// each `QueryRow` produced by the scan.
46    ///
47    /// **Wired by**: issue #692 (executor evaluation).  Until then, callers
48    /// can set this flag manually in tests or driver code.
49    pub include_cell_metadata: bool,
50}
51
52/// Query result containing rows and metadata
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct QueryResult {
55    /// Result rows
56    pub rows: Vec<QueryRow>,
57    /// Number of rows affected (for INSERT/UPDATE/DELETE)
58    pub rows_affected: u64,
59    /// Query execution time in milliseconds
60    pub execution_time_ms: u64,
61    /// Query metadata
62    pub metadata: QueryMetadata,
63}
64
65/// Individual row in query result
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct QueryRow {
68    /// Column values mapped by column name.
69    ///
70    /// Keyed by a shared `Arc<str>` column-name handle (issue #1334) interned
71    /// once by the decoder, so carrying a decoded cell's name into the row is a
72    /// reference-count bump rather than a per-cell heap `String` allocation.
73    /// `Arc<str>: Borrow<str>`, so all name-based reads (`get(&str)`, `keys()`,
74    /// iteration) are source-compatible with the prior `String` key. serde's
75    /// `rc` feature makes this (de)serialize with the identical name→value JSON
76    /// object shape.
77    pub values: HashMap<Arc<str>, Value>,
78    /// Original row key
79    pub key: RowKey,
80    /// Row metadata
81    pub metadata: RowMetadata,
82    /// Per-cell write metadata (Issue #691).
83    ///
84    /// Populated **only** when `ProjectionFlags::include_cell_metadata` is
85    /// `true` during query planning.  `None` on the hot path — no allocation
86    /// is performed unless metadata is explicitly requested.
87    ///
88    /// Map key = column name; value = write timestamp + optional expiration.
89    /// Columns absent from this map either had no individual cell header in
90    /// the SSTable (e.g. partition-key columns) or were not decoded under the
91    /// current flag setting.
92    #[serde(skip_serializing_if = "Option::is_none", default)]
93    pub cell_metadata: Option<HashMap<String, CellWriteMetadata>>,
94}
95
96/// Metadata for query results
97#[derive(Debug, Clone, Default, Serialize, Deserialize)]
98pub struct QueryMetadata {
99    /// Column information
100    pub columns: Vec<ColumnInfo>,
101    /// Total row count (may be different from returned rows due to LIMIT)
102    pub total_rows: Option<u64>,
103    /// Query execution plan information
104    pub plan_info: Option<PlanInfo>,
105    /// Performance metrics
106    pub performance: PerformanceMetrics,
107    /// Warnings generated during execution
108    pub warnings: Vec<String>,
109    /// Access path selected by the SSTable-scan step (Issue #960).
110    ///
111    /// `Some` when a SELECT ran through the modern `SelectExecutor` (materializing
112    /// or streaming); `None` for surfaces that do not yet report a path (e.g. the
113    /// legacy executor and non-SELECT queries). This is the result-attached half
114    /// of the access-path signal; the test-accessible probe is
115    /// `crate::query::access_path::last()`.
116    #[serde(skip_serializing_if = "Option::is_none", default)]
117    pub access_path: Option<crate::query::access_path::AccessPath>,
118}
119
120/// Information about a column in the result set
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct ColumnInfo {
123    /// Column name
124    pub name: String,
125    /// Column data type (flat, kept for backward compatibility)
126    pub data_type: crate::types::DataType,
127    /// Whether column can be null
128    pub nullable: bool,
129    /// Column position in result set
130    pub position: usize,
131    /// Original table name (for joined queries)
132    pub table_name: Option<String>,
133    /// Full schema-sourced CQL type (populated when a schema is available).
134    ///
135    /// This field expresses element types for collections (`list<int>`,
136    /// `map<text, bigint>`), and carries variants absent from the flat
137    /// `DataType` enum (`date`, `time`, `decimal`, `varint`, `counter`,
138    /// `duration`, `inet`). Downstream writers MUST use this over
139    /// `data_type` when it is `Some` — the no-heuristics mandate (Issue #28)
140    /// requires authoritative-schema metadata rather than runtime inference.
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub cql_type: Option<CqlType>,
143}
144
145/// Row metadata
146#[derive(Debug, Clone, Default, Serialize, Deserialize)]
147pub struct RowMetadata {
148    /// Row version/timestamp
149    pub version: Option<u64>,
150    /// Row TTL (time to live)
151    pub ttl: Option<u64>,
152    /// Row tags or labels
153    pub tags: HashMap<String, String>,
154}
155
156/// Query execution plan information
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct PlanInfo {
159    /// Plan type used
160    pub plan_type: String,
161    /// Estimated cost
162    pub estimated_cost: f64,
163    /// Actual cost
164    pub actual_cost: f64,
165    /// Indexes used
166    pub indexes_used: Vec<String>,
167    /// Steps executed
168    pub steps: Vec<String>,
169    /// Parallelization information
170    pub parallelization: Option<ParallelizationInfo>,
171}
172
173/// Parallelization information for query execution
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct ParallelizationInfo {
176    /// Number of threads used
177    pub threads_used: usize,
178    /// Whether parallelization was effective
179    pub effective: bool,
180    /// Partition information
181    pub partitions: Vec<PartitionInfo>,
182}
183
184/// Information about a partition processed in parallel
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct PartitionInfo {
187    /// Partition ID
188    pub id: usize,
189    /// Rows processed by this partition
190    pub rows_processed: u64,
191    /// Processing time for this partition
192    pub processing_time_ms: u64,
193}
194
195/// Performance metrics for query execution
196#[derive(Debug, Clone, Default, Serialize, Deserialize)]
197pub struct PerformanceMetrics {
198    /// Parse time in microseconds
199    pub parse_time_us: u64,
200    /// Planning time in microseconds
201    pub planning_time_us: u64,
202    /// Execution time in microseconds
203    pub execution_time_us: u64,
204    /// Total time in microseconds
205    pub total_time_us: u64,
206    /// Memory usage in bytes
207    pub memory_usage_bytes: u64,
208    /// I/O operations performed
209    pub io_operations: u64,
210    /// Cache hits
211    pub cache_hits: u64,
212    /// Cache misses
213    pub cache_misses: u64,
214}
215
216// ============================================================================
217// Streaming Query Results (Issue #280)
218// ============================================================================
219
220/// Configuration for streaming query results
221///
222/// Controls buffer sizes and chunk sizes for memory-efficient processing
223/// of large result sets.
224#[derive(Debug, Clone)]
225pub struct StreamingConfig {
226    /// Channel buffer size (controls backpressure)
227    /// Default: 1024 rows in flight
228    pub buffer_size: usize,
229    /// Chunk size hint for writers (rows per chunk)
230    /// Default: 10,000 rows (matches Parquet row group size)
231    pub chunk_size: usize,
232}
233
234impl Default for StreamingConfig {
235    fn default() -> Self {
236        Self {
237            buffer_size: 1024,  // 1K rows in flight
238            chunk_size: 10_000, // 10K rows per chunk (matches Parquet row group)
239        }
240    }
241}
242
243impl StreamingConfig {
244    /// Create a new streaming config with custom settings
245    pub fn new(buffer_size: usize, chunk_size: usize) -> Self {
246        Self {
247            buffer_size,
248            chunk_size,
249        }
250    }
251
252    /// Create a config optimized for Parquet output
253    pub fn for_parquet() -> Self {
254        Self {
255            buffer_size: 1024,
256            chunk_size: 10_000, // Row group size
257        }
258    }
259
260    /// Create a config optimized for CSV/JSON output
261    pub fn for_text_formats() -> Self {
262        Self {
263            buffer_size: 512,
264            chunk_size: 5_000, // Smaller chunks for text formats
265        }
266    }
267}
268
269/// Streaming query result iterator for memory-efficient processing
270///
271/// Instead of materializing all rows into a `Vec`, this iterator yields rows
272/// lazily via a channel, allowing processing of arbitrarily large result sets
273/// within the 128MB memory budget.
274///
275/// # Memory Budget
276///
277/// To stay within the 128MB target, callers MUST create a bounded channel
278/// with capacity from `StreamingConfig::buffer_size`. Assuming average row
279/// size of 1KB:
280/// - `buffer_size: 1024` = ~1MB in flight
281/// - `chunk_size: 10_000` = ~10MB per chunk
282/// - Total peak usage: ~11MB (well within 128MB budget)
283///
284/// For rows with large blobs/text, reduce buffer sizes proportionally.
285///
286/// # Contract
287///
288/// 1. The caller MUST create a bounded channel with `mpsc::channel(config.buffer_size)`
289/// 2. The iterator does NOT own the sender; the caller must spawn a task to send rows
290/// 3. The iterator is consumed once; create a new one for subsequent queries
291///
292/// # Example
293///
294/// ```ignore
295/// let config = StreamingConfig::default();
296/// let (tx, rx) = tokio::sync::mpsc::channel(config.buffer_size);
297///
298/// // Spawn producer
299/// tokio::spawn(async move {
300///     for row in rows {
301///         if tx.send(Ok(row)).await.is_err() {
302///             break; // Consumer dropped
303///         }
304///     }
305/// });
306///
307/// // Create iterator from receiver
308/// let mut iterator = QueryResultIterator::new(rx, metadata);
309///
310/// while let Some(row_result) = iterator.next_async().await {
311///     let row = row_result?;
312///     writer.write_row(&row)?;
313/// }
314/// ```
315pub struct QueryResultIterator {
316    /// Channel receiver for rows
317    receiver: mpsc::Receiver<Result<QueryRow, crate::Error>>,
318    /// Query metadata (columns, etc.)
319    pub metadata: QueryMetadata,
320    /// Total rows hint (if known from query planning)
321    pub total_rows_hint: Option<u64>,
322    /// Count of rows received so far
323    rows_received: u64,
324}
325
326impl QueryResultIterator {
327    /// Create a new streaming result iterator
328    pub fn new(
329        receiver: mpsc::Receiver<Result<QueryRow, crate::Error>>,
330        metadata: QueryMetadata,
331    ) -> Self {
332        Self {
333            receiver,
334            metadata,
335            total_rows_hint: None,
336            rows_received: 0,
337        }
338    }
339
340    /// Create with a known total row count hint
341    pub fn with_total_hint(mut self, total: u64) -> Self {
342        self.total_rows_hint = Some(total);
343        self
344    }
345
346    /// Receive next row (async)
347    ///
348    /// Returns `None` when all rows have been received.
349    pub async fn next_async(&mut self) -> Option<Result<QueryRow, crate::Error>> {
350        let result = self.receiver.recv().await?;
351        if result.is_ok() {
352            self.rows_received += 1;
353        }
354        Some(result)
355    }
356
357    /// Maximum allowed chunk size to prevent OOM
358    const MAX_CHUNK_SIZE: usize = 100_000;
359
360    /// Collect into chunks of specified size
361    ///
362    /// Returns a chunk of rows up to `size`. May return fewer rows if the
363    /// stream ends or an error occurs.
364    ///
365    /// # Arguments
366    ///
367    /// * `size` - Maximum number of rows to collect. Limited to MAX_CHUNK_SIZE
368    ///   (100,000) to prevent unbounded memory allocation.
369    ///
370    /// # Returns
371    ///
372    /// A vector of rows, which may be smaller than `size` if the stream ends
373    /// or an error occurs.
374    pub async fn collect_chunk(&mut self, size: usize) -> Result<Vec<QueryRow>, crate::Error> {
375        let safe_size = size.min(Self::MAX_CHUNK_SIZE);
376        // Grow the Vec lazily; a requested `safe_size` is only an upper bound.
377        let mut chunk = Vec::new();
378        while chunk.len() < safe_size {
379            match self.receiver.recv().await {
380                Some(Ok(row)) => {
381                    self.rows_received += 1;
382                    chunk.push(row);
383                }
384                Some(Err(e)) => return Err(e),
385                None => break,
386            }
387        }
388        Ok(chunk)
389    }
390
391    /// Get count of rows received so far
392    pub fn rows_received(&self) -> u64 {
393        self.rows_received
394    }
395
396    /// Get progress as a percentage (if total is known)
397    pub fn progress_percent(&self) -> Option<f64> {
398        self.total_rows_hint.map(|total| {
399            if total == 0 {
400                100.0
401            } else {
402                (self.rows_received as f64 / total as f64) * 100.0
403            }
404        })
405    }
406}
407
408impl QueryResult {
409    /// Create a new empty query result
410    pub fn new() -> Self {
411        Self {
412            rows: Vec::new(),
413            rows_affected: 0,
414            execution_time_ms: 0,
415            metadata: QueryMetadata::default(),
416        }
417    }
418
419    /// Create a result with rows
420    pub fn with_rows(rows: Vec<QueryRow>) -> Self {
421        Self {
422            rows,
423            ..Self::new()
424        }
425    }
426
427    /// Create a result for DML operations (INSERT/UPDATE/DELETE)
428    pub fn with_affected_rows(rows_affected: u64) -> Self {
429        Self {
430            rows_affected,
431            ..Self::new()
432        }
433    }
434
435    /// Get the number of rows in the result
436    pub fn row_count(&self) -> usize {
437        self.rows.len()
438    }
439
440    /// Check if the result is empty
441    pub fn is_empty(&self) -> bool {
442        self.rows.is_empty()
443    }
444
445    /// Get a specific row by index
446    pub fn get_row(&self, index: usize) -> Option<&QueryRow> {
447        self.rows.get(index)
448    }
449
450    /// Get column information
451    pub fn columns(&self) -> &[ColumnInfo] {
452        &self.metadata.columns
453    }
454
455    /// Get column names
456    pub fn column_names(&self) -> Vec<String> {
457        self.metadata
458            .columns
459            .iter()
460            .map(|c| c.name.clone())
461            .collect()
462    }
463
464    /// Get execution time in milliseconds
465    pub fn execution_time(&self) -> u64 {
466        self.execution_time_ms
467    }
468
469    /// Get performance metrics
470    pub fn performance(&self) -> &PerformanceMetrics {
471        &self.metadata.performance
472    }
473
474    /// Get warnings
475    pub fn warnings(&self) -> &[String] {
476        &self.metadata.warnings
477    }
478
479    /// Add a warning
480    pub fn add_warning(&mut self, warning: String) {
481        self.metadata.warnings.push(warning);
482    }
483
484    /// Convert to JSON representation
485    ///
486    /// Note: `execution_time_ms` is intentionally excluded to keep snapshot
487    /// output deterministic; use `execution_time()` to read it separately.
488    pub fn to_json(&self) -> serde_json::Value {
489        let rows: Vec<_> = self
490            .rows
491            .iter()
492            .map(|row| self.row_to_json_deterministic(row))
493            .collect();
494        let columns: Vec<_> = self
495            .metadata
496            .columns
497            .iter()
498            .map(ColumnInfo::to_json)
499            .collect();
500        let warnings: Vec<_> = self
501            .metadata
502            .warnings
503            .iter()
504            .cloned()
505            .map(serde_json::Value::String)
506            .collect();
507
508        json!({
509            "rows": rows,
510            "rows_affected": self.rows_affected,
511            "row_count": self.rows.len(),
512            "columns": columns,
513            "performance": self.metadata.performance.to_json(),
514            "warnings": warnings,
515        })
516    }
517
518    /// Create result iterator
519    pub fn iter(&self) -> std::slice::Iter<'_, QueryRow> {
520        self.rows.iter()
521    }
522
523    /// Convert a single row to JSON with deterministic field ordering.
524    ///
525    /// When `metadata.columns` is populated, fields appear in that order; otherwise
526    /// HashMap keys are emitted in sorted order so snapshots stay stable.
527    fn row_to_json_deterministic(&self, row: &QueryRow) -> serde_json::Value {
528        let mut result = serde_json::Map::new();
529
530        if !self.metadata.columns.is_empty() {
531            for col in &self.metadata.columns {
532                let value_json = row
533                    .values
534                    .get(col.name.as_str())
535                    .map_or(serde_json::Value::Null, ToJson::to_json);
536                result.insert(col.name.clone(), value_json);
537            }
538        } else {
539            let mut sorted_keys: Vec<&Arc<str>> = row.values.keys().collect();
540            sorted_keys.sort();
541            for key in sorted_keys {
542                if let Some(value) = row.values.get(key.as_ref()) {
543                    result.insert(key.to_string(), value.to_json());
544                }
545            }
546        }
547
548        result.insert(
549            "_key".to_string(),
550            serde_json::Value::String(format!("{:?}", row.key)),
551        );
552
553        if row_metadata_is_populated(&row.metadata) {
554            result.insert("_metadata".to_string(), row.metadata.to_json());
555        }
556
557        serde_json::Value::Object(result)
558    }
559}
560
561impl QueryRow {
562    /// Create a new query row
563    pub fn new(key: RowKey) -> Self {
564        Self {
565            values: HashMap::new(),
566            key,
567            metadata: RowMetadata::default(),
568            cell_metadata: None,
569        }
570    }
571
572    /// Create a row with values.
573    ///
574    /// Concrete over `HashMap<String, Value>` so existing callers (including
575    /// `QueryRow::with_values(key, HashMap::new())`) infer the key type without
576    /// annotations (issue #1334); the string keys are interned into shared
577    /// `Arc<str>` handles once here. Callers that already hold interned
578    /// `Arc<str>` keys should use [`QueryRow::with_interned_values`] to skip the
579    /// re-allocation.
580    pub fn with_values(key: RowKey, values: HashMap<String, Value>) -> Self {
581        Self {
582            values: values.into_iter().map(|(k, v)| (Arc::from(k), v)).collect(),
583            key,
584            metadata: RowMetadata::default(),
585            cell_metadata: None,
586        }
587    }
588
589    /// Create a row from already-interned `Arc<str>` column-name handles.
590    ///
591    /// The interned-key counterpart of [`QueryRow::with_values`] (issue #1334):
592    /// the storage/scan path already carries interned `Arc<str>` names, so the
593    /// handles move straight in with only a reference-count bump — no per-cell
594    /// `String` allocation.
595    pub fn with_interned_values(key: RowKey, values: HashMap<Arc<str>, Value>) -> Self {
596        Self {
597            values,
598            key,
599            metadata: RowMetadata::default(),
600            cell_metadata: None,
601        }
602    }
603
604    /// Create a row from a column name → value map, using a synthetic empty key.
605    ///
606    /// Convenience constructor used by CLI utilities that do not track a raw
607    /// partition key.  The key is set to an empty byte vector. Concrete over
608    /// `HashMap<String, Value>` so `HashMap::new()` callers infer without
609    /// annotations (issue #1334); the string keys are interned here.
610    pub fn from_map(values: HashMap<String, Value>) -> Self {
611        Self {
612            values: values.into_iter().map(|(k, v)| (Arc::from(k), v)).collect(),
613            key: RowKey::new(vec![]),
614            metadata: RowMetadata::default(),
615            cell_metadata: None,
616        }
617    }
618
619    /// Get a value by column name
620    pub fn get(&self, column: &str) -> Option<&Value> {
621        self.values.get(column)
622    }
623
624    /// Set a value for a column.
625    ///
626    /// Accepts anything convertible into the shared `Arc<str>` key (issue
627    /// #1334): a `String`, `&str`, or an existing `Arc<str>` handle.
628    pub fn set(&mut self, column: impl Into<Arc<str>>, value: Value) {
629        self.values.insert(column.into(), value);
630    }
631
632    /// Get all column names
633    pub fn column_names(&self) -> Vec<String> {
634        self.values.keys().map(|k| k.to_string()).collect()
635    }
636
637    /// Get the row key
638    pub fn key(&self) -> &RowKey {
639        &self.key
640    }
641
642    /// Get row metadata
643    pub fn metadata(&self) -> &RowMetadata {
644        &self.metadata
645    }
646
647    /// Set row metadata
648    pub fn set_metadata(&mut self, metadata: RowMetadata) {
649        self.metadata = metadata;
650    }
651
652    // ---- Per-cell metadata (Issue #691) ----
653
654    /// Attach per-cell write metadata to this row.
655    ///
656    /// Replaces any previously attached map. Intended to be called by the
657    /// scan/build path when `ProjectionFlags::include_cell_metadata` is set.
658    pub fn set_cell_metadata(&mut self, map: HashMap<String, CellWriteMetadata>) {
659        self.cell_metadata = Some(map);
660    }
661
662    /// Insert a single column's write metadata.
663    ///
664    /// Initialises the map on first call; subsequent calls insert into the
665    /// existing map.  No-op when called on the hot path that never enables
666    /// metadata (the caller guards on the flag).
667    pub fn insert_cell_metadata(&mut self, column: String, meta: CellWriteMetadata) {
668        self.cell_metadata
669            .get_or_insert_with(HashMap::new)
670            .insert(column, meta);
671    }
672
673    /// Return the write metadata for `column`, if present.
674    pub fn get_cell_metadata(&self, column: &str) -> Option<&CellWriteMetadata> {
675        self.cell_metadata.as_ref()?.get(column)
676    }
677
678    /// Convert to JSON representation
679    pub fn to_json(&self) -> serde_json::Value {
680        let mut result = serde_json::Map::new();
681
682        for (column, value) in &self.values {
683            result.insert(column.to_string(), value.to_json());
684        }
685
686        result.insert(
687            "_key".to_string(),
688            serde_json::Value::String(format!("{:?}", self.key)),
689        );
690
691        if row_metadata_is_populated(&self.metadata) {
692            result.insert("_metadata".to_string(), self.metadata.to_json());
693        }
694
695        serde_json::Value::Object(result)
696    }
697}
698
699impl ColumnInfo {
700    /// Create new column info
701    pub fn new(
702        name: String,
703        data_type: crate::types::DataType,
704        nullable: bool,
705        position: usize,
706    ) -> Self {
707        Self {
708            name,
709            data_type,
710            nullable,
711            position,
712            table_name: None,
713            cql_type: None,
714        }
715    }
716
717    /// Set table name
718    pub fn with_table_name(mut self, table_name: String) -> Self {
719        self.table_name = Some(table_name);
720        self
721    }
722
723    /// Attach a schema-sourced [`CqlType`] to this column.
724    ///
725    /// The `data_type` field is left unchanged so existing consumers remain
726    /// unaffected; downstream writers that need full type fidelity (e.g. the
727    /// Parquet/Arrow writer) should prefer `cql_type` when it is `Some`.
728    pub fn with_cql_type(mut self, cql_type: CqlType) -> Self {
729        self.cql_type = Some(cql_type);
730        self
731    }
732
733    /// Convert to JSON representation
734    ///
735    /// The `data_type` and all pre-existing keys are preserved unchanged for
736    /// backward compatibility. The new `cql_type` key is only emitted when
737    /// the field is `Some` (it is marked `skip_serializing_if` in the struct
738    /// derive, but we also reflect it here for the hand-rolled JSON path).
739    pub fn to_json(&self) -> serde_json::Value {
740        let mut map = serde_json::Map::new();
741        map.insert("name".to_string(), json!(self.name));
742        map.insert(
743            "data_type".to_string(),
744            json!(format!("{:?}", self.data_type)),
745        );
746        map.insert("nullable".to_string(), json!(self.nullable));
747        map.insert("position".to_string(), json!(self.position));
748        if let Some(table_name) = &self.table_name {
749            map.insert("table_name".to_string(), json!(table_name));
750        }
751        // New additive key: only present when schema CqlType is known.
752        if let Some(cql_type) = &self.cql_type {
753            map.insert("cql_type".to_string(), json!(format_cql_type(cql_type)));
754        }
755        serde_json::Value::Object(map)
756    }
757}
758
759/// Map a schema-level [`CqlType`] to the flat [`crate::types::DataType`] enum.
760///
761/// This is used in the `SELECT *` path and explicit projection paths to derive
762/// a backward-compatible `data_type` from authoritative schema metadata rather
763/// than hard-coding `DataType::Text` (Issue #674 / no-heuristics mandate #28).
764pub fn cql_type_to_data_type(cql_type: &CqlType) -> crate::types::DataType {
765    use crate::types::DataType;
766    match cql_type {
767        CqlType::Boolean => DataType::Boolean,
768        CqlType::TinyInt => DataType::TinyInt,
769        CqlType::SmallInt => DataType::SmallInt,
770        CqlType::Int => DataType::Integer,
771        CqlType::BigInt | CqlType::Varint | CqlType::Counter => DataType::BigInt,
772        CqlType::Float => DataType::Float32,
773        CqlType::Double | CqlType::Decimal => DataType::Float,
774        CqlType::Text | CqlType::Varchar | CqlType::Ascii => DataType::Text,
775        CqlType::Blob => DataType::Blob,
776        CqlType::Timestamp => DataType::Timestamp,
777        CqlType::Date | CqlType::Time | CqlType::Duration | CqlType::Inet => DataType::BigInt,
778        CqlType::Uuid | CqlType::TimeUuid => DataType::Uuid,
779        CqlType::List(_) => DataType::List,
780        CqlType::Set(_) => DataType::Set,
781        CqlType::Map(_, _) => DataType::Map,
782        CqlType::Tuple(_) => DataType::Tuple,
783        CqlType::Udt(_, _) => DataType::Udt,
784        CqlType::Frozen(inner) => cql_type_to_data_type(inner),
785        CqlType::Custom(_) => DataType::Blob,
786    }
787}
788
789/// Format a [`CqlType`] as a human-readable CQL type string.
790///
791/// Used for the `cql_type` field in `ColumnInfo::to_json`.
792fn format_cql_type(cql_type: &CqlType) -> String {
793    match cql_type {
794        CqlType::Boolean => "boolean".to_string(),
795        CqlType::TinyInt => "tinyint".to_string(),
796        CqlType::SmallInt => "smallint".to_string(),
797        CqlType::Int => "int".to_string(),
798        CqlType::BigInt => "bigint".to_string(),
799        CqlType::Counter => "counter".to_string(),
800        CqlType::Float => "float".to_string(),
801        CqlType::Double => "double".to_string(),
802        CqlType::Decimal => "decimal".to_string(),
803        CqlType::Text => "text".to_string(),
804        CqlType::Varchar => "varchar".to_string(),
805        CqlType::Ascii => "ascii".to_string(),
806        CqlType::Blob => "blob".to_string(),
807        CqlType::Timestamp => "timestamp".to_string(),
808        CqlType::Date => "date".to_string(),
809        CqlType::Time => "time".to_string(),
810        CqlType::Uuid => "uuid".to_string(),
811        CqlType::TimeUuid => "timeuuid".to_string(),
812        CqlType::Inet => "inet".to_string(),
813        CqlType::Duration => "duration".to_string(),
814        CqlType::Varint => "varint".to_string(),
815        CqlType::List(inner) => format!("list<{}>", format_cql_type(inner)),
816        CqlType::Set(inner) => format!("set<{}>", format_cql_type(inner)),
817        CqlType::Map(k, v) => format!("map<{}, {}>", format_cql_type(k), format_cql_type(v)),
818        CqlType::Tuple(types) => {
819            let inner: Vec<_> = types.iter().map(format_cql_type).collect();
820            format!("tuple<{}>", inner.join(", "))
821        }
822        CqlType::Udt(name, _) => name.clone(),
823        CqlType::Frozen(inner) => format!("frozen<{}>", format_cql_type(inner)),
824        CqlType::Custom(name) => name.clone(),
825    }
826}
827
828impl RowMetadata {
829    /// Create new row metadata
830    pub fn new() -> Self {
831        Self::default()
832    }
833
834    /// Set version
835    pub fn with_version(mut self, version: u64) -> Self {
836        self.version = Some(version);
837        self
838    }
839
840    /// Set TTL
841    pub fn with_ttl(mut self, ttl: u64) -> Self {
842        self.ttl = Some(ttl);
843        self
844    }
845
846    /// Add tag
847    pub fn with_tag(mut self, key: String, value: String) -> Self {
848        self.tags.insert(key, value);
849        self
850    }
851
852    /// Convert to JSON representation
853    pub fn to_json(&self) -> serde_json::Value {
854        let mut map = serde_json::Map::new();
855        if let Some(version) = self.version {
856            map.insert("version".to_string(), json!(version));
857        }
858        if let Some(ttl) = self.ttl {
859            map.insert("ttl".to_string(), json!(ttl));
860        }
861        if !self.tags.is_empty() {
862            map.insert("tags".to_string(), json!(self.tags));
863        }
864        serde_json::Value::Object(map)
865    }
866}
867
868impl PerformanceMetrics {
869    /// Create new performance metrics
870    pub fn new() -> Self {
871        Self::default()
872    }
873
874    /// Get total time in milliseconds
875    pub fn total_time_ms(&self) -> u64 {
876        self.total_time_us / 1000
877    }
878
879    /// Get cache hit ratio
880    pub fn cache_hit_ratio(&self) -> f64 {
881        let total = self.cache_hits + self.cache_misses;
882        if total == 0 {
883            0.0
884        } else {
885            self.cache_hits as f64 / total as f64
886        }
887    }
888
889    /// Convert to JSON representation
890    pub fn to_json(&self) -> serde_json::Value {
891        let cache_hit_ratio = serde_json::Number::from_f64(self.cache_hit_ratio())
892            .map(serde_json::Value::Number)
893            .unwrap_or(json!(0));
894        json!({
895            "parse_time_us": self.parse_time_us,
896            "planning_time_us": self.planning_time_us,
897            "execution_time_us": self.execution_time_us,
898            "total_time_us": self.total_time_us,
899            "memory_usage_bytes": self.memory_usage_bytes,
900            "io_operations": self.io_operations,
901            "cache_hits": self.cache_hits,
902            "cache_misses": self.cache_misses,
903            "cache_hit_ratio": cache_hit_ratio,
904        })
905    }
906}
907
908/// Write one horizontal border row using the supplied left/middle/right glyphs.
909fn write_border(
910    f: &mut fmt::Formatter<'_>,
911    widths: &[usize],
912    left: char,
913    sep: char,
914    right: char,
915) -> fmt::Result {
916    write!(f, "{}", left)?;
917    for (i, width) in widths.iter().enumerate() {
918        write!(f, "{}", "─".repeat(width + 2))?;
919        if i < widths.len() - 1 {
920            write!(f, "{}", sep)?;
921        }
922    }
923    writeln!(f, "{}", right)
924}
925
926impl fmt::Display for QueryResult {
927    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
928        if self.rows.is_empty() {
929            return write!(f, "Empty result set ({} rows affected)", self.rows_affected);
930        }
931
932        let column_names = self.column_names();
933        if column_names.is_empty() {
934            return write!(f, "No columns in result set");
935        }
936
937        // Column widths = max(header, longest value) for each column.
938        let col_widths: Vec<usize> = column_names
939            .iter()
940            .map(|col_name| {
941                self.rows
942                    .iter()
943                    .filter_map(|row| row.values.get(col_name.as_str()))
944                    .map(|v| format!("{}", v).len())
945                    .max()
946                    .unwrap_or(0)
947                    .max(col_name.len())
948            })
949            .collect();
950
951        write_border(f, &col_widths, '┌', '┬', '┐')?;
952
953        write!(f, "│")?;
954        for (i, (col_name, width)) in column_names.iter().zip(col_widths.iter()).enumerate() {
955            write!(f, " {:width$} ", col_name, width = width)?;
956            if i < column_names.len() - 1 {
957                write!(f, "│")?;
958            }
959        }
960        writeln!(f, "│")?;
961
962        write_border(f, &col_widths, '├', '┼', '┤')?;
963
964        for row in &self.rows {
965            write!(f, "│")?;
966            for (i, (col_name, width)) in column_names.iter().zip(col_widths.iter()).enumerate() {
967                let value = row
968                    .values
969                    .get(col_name.as_str())
970                    .map(|v| format!("{}", v))
971                    .unwrap_or_else(|| "NULL".to_string());
972                write!(f, " {:width$} ", value, width = width)?;
973                if i < column_names.len() - 1 {
974                    write!(f, "│")?;
975                }
976            }
977            writeln!(f, "│")?;
978        }
979
980        write_border(f, &col_widths, '└', '┴', '┘')?;
981
982        writeln!(
983            f,
984            "{} rows returned in {}ms",
985            self.rows.len(),
986            self.execution_time_ms
987        )?;
988
989        if !self.metadata.warnings.is_empty() {
990            writeln!(f, "\nWarnings:")?;
991            for warning in &self.metadata.warnings {
992                writeln!(f, "  - {}", warning)?;
993            }
994        }
995
996        Ok(())
997    }
998}
999
1000impl Default for QueryResult {
1001    fn default() -> Self {
1002        Self::new()
1003    }
1004}
1005
1006impl IntoIterator for QueryResult {
1007    type Item = QueryRow;
1008    type IntoIter = std::vec::IntoIter<QueryRow>;
1009
1010    fn into_iter(self) -> Self::IntoIter {
1011        self.rows.into_iter()
1012    }
1013}
1014
1015impl<'a> IntoIterator for &'a QueryResult {
1016    type Item = &'a QueryRow;
1017    type IntoIter = std::slice::Iter<'a, QueryRow>;
1018
1019    fn into_iter(self) -> Self::IntoIter {
1020        self.rows.iter()
1021    }
1022}
1023
1024// Helper trait for converting values to JSON
1025trait ToJson {
1026    fn to_json(&self) -> serde_json::Value;
1027}
1028
1029impl ToJson for Value {
1030    fn to_json(&self) -> serde_json::Value {
1031        // Non-finite floats have no JSON representation; we emit null for those.
1032        fn float_to_json(x: f64) -> serde_json::Value {
1033            serde_json::Number::from_f64(x)
1034                .map(serde_json::Value::Number)
1035                .unwrap_or(serde_json::Value::Null)
1036        }
1037
1038        match self {
1039            Value::Null => serde_json::Value::Null,
1040            Value::Boolean(b) => json!(*b),
1041            Value::Integer(i) => json!(*i),
1042            Value::BigInt(i) => json!(*i),
1043            Value::Counter(c) => json!(*c),
1044            Value::TinyInt(i) => json!(*i as i64),
1045            Value::SmallInt(i) => json!(*i as i64),
1046            Value::Date(d) => json!(*d),
1047            Value::Time(t) => json!(*t),
1048            Value::Timestamp(ts) => json!(*ts),
1049            Value::Float(f) => float_to_json(*f),
1050            Value::Float32(f) => float_to_json(*f as f64),
1051            Value::Text(s) => json!(std::str::from_utf8(s).unwrap_or_default()),
1052            Value::Json(value) => (**value).clone(),
1053            Value::Blob(bytes) | Value::Varint(bytes) | Value::Inet(bytes) => json!(b64(bytes)),
1054            Value::Uuid(uuid) => json!(b64(uuid)),
1055            Value::List(items) | Value::Set(items) | Value::Tuple(items) => {
1056                let json_list: Vec<_> = items.iter().map(ToJson::to_json).collect();
1057                serde_json::Value::Array(json_list)
1058            }
1059            Value::Map(entries) => {
1060                let json_map: serde_json::Map<String, serde_json::Value> = entries
1061                    .iter()
1062                    .map(|(k, v)| (format!("{}", k), v.to_json()))
1063                    .collect();
1064                serde_json::Value::Object(json_map)
1065            }
1066            Value::Udt(udt) => {
1067                let mut json_obj = serde_json::Map::new();
1068                json_obj.insert("_type".to_string(), json!(udt.type_name));
1069                for field in &udt.fields {
1070                    let field_json = field
1071                        .value
1072                        .as_ref()
1073                        .map_or(serde_json::Value::Null, ToJson::to_json);
1074                    json_obj.insert(field.name.clone(), field_json);
1075                }
1076                serde_json::Value::Object(json_obj)
1077            }
1078            Value::Frozen(boxed) => boxed.to_json(),
1079            Value::Decimal { scale, unscaled } => json!({
1080                "scale": *scale,
1081                "unscaled": b64(unscaled),
1082            }),
1083            Value::Duration {
1084                months,
1085                days,
1086                nanos,
1087            } => json!({
1088                "months": *months,
1089                "days": *days,
1090                "nanos": *nanos,
1091            }),
1092            Value::Tombstone(info) => {
1093                let mut json_obj = serde_json::Map::new();
1094                json_obj.insert("type".to_string(), json!("tombstone"));
1095                json_obj.insert("deletion_time".to_string(), json!(info.deletion_time));
1096                json_obj.insert(
1097                    "tombstone_type".to_string(),
1098                    json!(format!("{:?}", info.tombstone_type)),
1099                );
1100                if let Some(ttl) = info.ttl {
1101                    json_obj.insert("ttl".to_string(), json!(ttl));
1102                }
1103                serde_json::Value::Object(json_obj)
1104            }
1105        }
1106    }
1107}
1108
1109#[cfg(test)]
1110mod tests {
1111    use super::*;
1112    use crate::Value;
1113
1114    #[test]
1115    fn test_query_result_creation() {
1116        let result = QueryResult::new();
1117        assert!(result.is_empty());
1118        assert_eq!(result.row_count(), 0);
1119        assert_eq!(result.execution_time(), 0);
1120    }
1121
1122    #[test]
1123    fn test_query_result_with_rows() {
1124        let mut row1 = QueryRow::new(RowKey::new(vec![1]));
1125        row1.set("id".to_string(), Value::Integer(1));
1126        row1.set("name".to_string(), Value::text("Alice".to_string()));
1127
1128        let mut row2 = QueryRow::new(RowKey::new(vec![2]));
1129        row2.set("id".to_string(), Value::Integer(2));
1130        row2.set("name".to_string(), Value::text("Bob".to_string()));
1131
1132        let result = QueryResult::with_rows(vec![row1, row2]);
1133        assert_eq!(result.row_count(), 2);
1134        assert!(!result.is_empty());
1135
1136        let first_row = result.get_row(0).unwrap();
1137        assert_eq!(first_row.get("id"), Some(&Value::Integer(1)));
1138        assert_eq!(
1139            first_row.get("name"),
1140            Some(&Value::text("Alice".to_string()))
1141        );
1142    }
1143
1144    #[test]
1145    fn test_query_row_operations() {
1146        let mut row = QueryRow::new(RowKey::new(vec![1]));
1147        row.set("id".to_string(), Value::Integer(42));
1148        row.set("active".to_string(), Value::Boolean(true));
1149
1150        assert_eq!(row.get("id"), Some(&Value::Integer(42)));
1151        assert_eq!(row.get("active"), Some(&Value::Boolean(true)));
1152        assert_eq!(row.get("nonexistent"), None);
1153
1154        let column_names = row.column_names();
1155        assert_eq!(column_names.len(), 2);
1156        assert!(column_names.contains(&"id".to_string()));
1157        assert!(column_names.contains(&"active".to_string()));
1158    }
1159
1160    #[test]
1161    fn test_column_info() {
1162        let column = ColumnInfo::new(
1163            "user_id".to_string(),
1164            crate::types::DataType::Integer,
1165            false,
1166            0,
1167        )
1168        .with_table_name("users".to_string());
1169
1170        assert_eq!(column.name, "user_id");
1171        assert_eq!(column.data_type, crate::types::DataType::Integer);
1172        assert!(!column.nullable);
1173        assert_eq!(column.position, 0);
1174        assert_eq!(column.table_name, Some("users".to_string()));
1175        assert!(column.cql_type.is_none());
1176    }
1177
1178    #[test]
1179    fn test_column_info_with_cql_type_scalar() {
1180        use crate::schema::CqlType;
1181        let column = ColumnInfo::new("ts".to_string(), crate::types::DataType::Timestamp, true, 1)
1182            .with_cql_type(CqlType::Timestamp);
1183
1184        assert_eq!(column.name, "ts");
1185        assert_eq!(column.cql_type, Some(CqlType::Timestamp));
1186        // data_type is unaffected
1187        assert_eq!(column.data_type, crate::types::DataType::Timestamp);
1188    }
1189
1190    #[test]
1191    fn test_column_info_with_cql_type_list() {
1192        use crate::schema::CqlType;
1193        let list_type = CqlType::List(Box::new(CqlType::Int));
1194        let column = ColumnInfo::new("items".to_string(), crate::types::DataType::List, true, 2)
1195            .with_cql_type(list_type.clone());
1196
1197        assert_eq!(column.cql_type, Some(list_type));
1198    }
1199
1200    #[test]
1201    fn test_column_info_with_cql_type_map() {
1202        use crate::schema::CqlType;
1203        let map_type = CqlType::Map(Box::new(CqlType::Text), Box::new(CqlType::BigInt));
1204        let column = ColumnInfo::new("props".to_string(), crate::types::DataType::Map, true, 3)
1205            .with_cql_type(map_type.clone());
1206
1207        assert_eq!(column.cql_type, Some(map_type));
1208    }
1209
1210    #[test]
1211    fn test_column_info_with_cql_type_udt() {
1212        use crate::schema::CqlType;
1213        let udt_type = CqlType::Udt("address".to_string(), vec![]);
1214        let column = ColumnInfo::new("addr".to_string(), crate::types::DataType::Udt, true, 4)
1215            .with_cql_type(udt_type.clone());
1216
1217        assert_eq!(column.cql_type, Some(udt_type));
1218    }
1219
1220    #[test]
1221    fn test_cql_type_to_data_type_scalars() {
1222        use super::cql_type_to_data_type;
1223        use crate::schema::CqlType;
1224        use crate::types::DataType;
1225
1226        assert_eq!(cql_type_to_data_type(&CqlType::Boolean), DataType::Boolean);
1227        assert_eq!(cql_type_to_data_type(&CqlType::Int), DataType::Integer);
1228        assert_eq!(cql_type_to_data_type(&CqlType::BigInt), DataType::BigInt);
1229        assert_eq!(cql_type_to_data_type(&CqlType::Text), DataType::Text);
1230        assert_eq!(cql_type_to_data_type(&CqlType::Blob), DataType::Blob);
1231        assert_eq!(cql_type_to_data_type(&CqlType::Uuid), DataType::Uuid);
1232        assert_eq!(
1233            cql_type_to_data_type(&CqlType::Timestamp),
1234            DataType::Timestamp
1235        );
1236    }
1237
1238    #[test]
1239    fn test_cql_type_to_data_type_collections() {
1240        use super::cql_type_to_data_type;
1241        use crate::schema::CqlType;
1242        use crate::types::DataType;
1243
1244        assert_eq!(
1245            cql_type_to_data_type(&CqlType::List(Box::new(CqlType::Int))),
1246            DataType::List
1247        );
1248        assert_eq!(
1249            cql_type_to_data_type(&CqlType::Set(Box::new(CqlType::Text))),
1250            DataType::Set
1251        );
1252        assert_eq!(
1253            cql_type_to_data_type(&CqlType::Map(
1254                Box::new(CqlType::Text),
1255                Box::new(CqlType::BigInt)
1256            )),
1257            DataType::Map
1258        );
1259    }
1260
1261    #[test]
1262    fn test_cql_type_to_data_type_frozen() {
1263        use super::cql_type_to_data_type;
1264        use crate::schema::CqlType;
1265        use crate::types::DataType;
1266
1267        // frozen<list<int>> should unwrap to DataType::List
1268        assert_eq!(
1269            cql_type_to_data_type(&CqlType::Frozen(Box::new(CqlType::List(Box::new(
1270                CqlType::Int
1271            ))))),
1272            DataType::List
1273        );
1274    }
1275
1276    #[test]
1277    fn test_column_info_to_json_includes_cql_type() {
1278        use crate::schema::CqlType;
1279        let column = ColumnInfo::new("items".to_string(), crate::types::DataType::List, true, 0)
1280            .with_cql_type(CqlType::List(Box::new(CqlType::Int)));
1281
1282        let json = column.to_json();
1283        let obj = json.as_object().unwrap();
1284
1285        // Existing keys unchanged
1286        assert_eq!(obj["name"], "items");
1287        assert!(obj.contains_key("data_type"));
1288        assert!(obj.contains_key("nullable"));
1289        assert!(obj.contains_key("position"));
1290
1291        // New key present
1292        assert!(obj.contains_key("cql_type"));
1293        assert_eq!(obj["cql_type"], "list<int>");
1294    }
1295
1296    #[test]
1297    fn test_column_info_to_json_no_cql_type() {
1298        // Without cql_type, the JSON must not include the key
1299        let column = ColumnInfo::new("id".to_string(), crate::types::DataType::Integer, false, 0);
1300
1301        let json = column.to_json();
1302        let obj = json.as_object().unwrap();
1303
1304        assert!(!obj.contains_key("cql_type"));
1305    }
1306
1307    #[test]
1308    fn test_row_metadata() {
1309        let metadata = RowMetadata::new()
1310            .with_version(123)
1311            .with_ttl(3600)
1312            .with_tag("source".to_string(), "import".to_string());
1313
1314        assert_eq!(metadata.version, Some(123));
1315        assert_eq!(metadata.ttl, Some(3600));
1316        assert_eq!(metadata.tags.get("source"), Some(&"import".to_string()));
1317    }
1318
1319    #[test]
1320    fn test_performance_metrics() {
1321        let mut metrics = PerformanceMetrics::new();
1322        metrics.cache_hits = 8;
1323        metrics.cache_misses = 2;
1324        metrics.total_time_us = 5000;
1325
1326        assert_eq!(metrics.cache_hit_ratio(), 0.8);
1327        assert_eq!(metrics.total_time_ms(), 5);
1328    }
1329
1330    #[test]
1331    fn test_json_serialization() {
1332        let mut row = QueryRow::new(RowKey::new(vec![1]));
1333        row.set("id".to_string(), Value::Integer(1));
1334        row.set("name".to_string(), Value::text("test".to_string()));
1335
1336        let json = row.to_json();
1337        assert!(json.is_object());
1338
1339        let obj = json.as_object().unwrap();
1340        assert_eq!(obj.get("id"), Some(&serde_json::Value::Number(1.into())));
1341        assert_eq!(
1342            obj.get("name"),
1343            Some(&serde_json::Value::String("test".to_string()))
1344        );
1345    }
1346
1347    #[test]
1348    fn test_result_iteration() {
1349        let row1 = QueryRow::new(RowKey::new(vec![1]));
1350        let row2 = QueryRow::new(RowKey::new(vec![2]));
1351        let result = QueryResult::with_rows(vec![row1, row2]);
1352
1353        let mut count = 0;
1354        for _row in &result {
1355            count += 1;
1356        }
1357        assert_eq!(count, 2);
1358
1359        let mut count = 0;
1360        for _row in result {
1361            count += 1;
1362        }
1363        assert_eq!(count, 2);
1364    }
1365
1366    // =========================================================================
1367    // Issue #691: per-cell writetime/TTL metadata plumbing tests
1368    // =========================================================================
1369
1370    /// Hot-path guarantee: a row constructed without setting cell metadata
1371    /// must have `cell_metadata == None` — no allocation.
1372    #[test]
1373    fn test_cell_metadata_absent_by_default() {
1374        let row = QueryRow::new(RowKey::new(vec![1]));
1375        assert!(
1376            row.cell_metadata.is_none(),
1377            "cell_metadata must be None when no metadata is attached (hot-path, zero allocation)"
1378        );
1379
1380        let row2 = QueryRow::with_values(RowKey::new(vec![2]), HashMap::new());
1381        assert!(row2.cell_metadata.is_none());
1382
1383        let row3 = QueryRow::from_map(HashMap::new());
1384        assert!(row3.cell_metadata.is_none());
1385    }
1386
1387    /// ProjectionFlags::default() must not request cell metadata.
1388    #[test]
1389    fn test_projection_flags_default_no_metadata() {
1390        let flags = ProjectionFlags::default();
1391        assert!(
1392            !flags.include_cell_metadata,
1393            "include_cell_metadata must default to false"
1394        );
1395    }
1396
1397    /// Single SSTable scenario: attach metadata to a row and read it back.
1398    #[test]
1399    fn test_cell_metadata_single_sstable_single_cell() {
1400        let mut row = QueryRow::new(RowKey::new(vec![1]));
1401        row.set("name".to_string(), Value::text("Alice".to_string()));
1402
1403        let meta = CellWriteMetadata {
1404            write_timestamp_micros: 1_700_000_000_000_000, // ~2023 epoch in µs
1405            expiration: None,
1406        };
1407        row.insert_cell_metadata("name".to_string(), meta.clone());
1408
1409        // cell_metadata map must now be Some
1410        assert!(row.cell_metadata.is_some());
1411        // Values are unchanged
1412        assert_eq!(row.get("name"), Some(&Value::text("Alice".to_string())));
1413        // Metadata round-trips correctly
1414        let got = row
1415            .get_cell_metadata("name")
1416            .expect("metadata must be present");
1417        assert_eq!(got.write_timestamp_micros, meta.write_timestamp_micros);
1418        assert!(got.expiration.is_none());
1419    }
1420
1421    /// TTL / expiration path: metadata includes expiry info.
1422    #[test]
1423    fn test_cell_metadata_with_ttl_expiration() {
1424        let mut row = QueryRow::new(RowKey::new(vec![2]));
1425        row.set("score".to_string(), Value::Integer(42));
1426
1427        let ttl_seconds = 3600_i32;
1428        let write_ts_micros = 1_700_000_000_000_000_i64;
1429        // expires_at = write_ts / 1_000_000 + ttl
1430        let expires_at = (write_ts_micros / 1_000_000) + ttl_seconds as i64;
1431
1432        let meta = CellWriteMetadata {
1433            write_timestamp_micros: write_ts_micros,
1434            expiration: Some(CellExpiration {
1435                ttl_seconds,
1436                expires_at_seconds: expires_at,
1437            }),
1438        };
1439        row.insert_cell_metadata("score".to_string(), meta);
1440
1441        let got = row.get_cell_metadata("score").unwrap();
1442        assert_eq!(got.write_timestamp_micros, write_ts_micros);
1443        let exp = got.expiration.as_ref().unwrap();
1444        assert_eq!(exp.ttl_seconds, 3600);
1445        assert_eq!(exp.expires_at_seconds, expires_at);
1446    }
1447
1448    /// Null cell: metadata is absent for columns not decoded (e.g. partition-key
1449    /// columns reconstructed from the raw key bytes, not from cells).
1450    #[test]
1451    fn test_cell_metadata_absent_for_null_cells() {
1452        let mut row = QueryRow::new(RowKey::new(vec![3]));
1453        row.set("id".to_string(), Value::Null);
1454        // We do NOT insert metadata for "id" — simulating a null/missing cell.
1455        // Even if metadata is enabled for other columns, this one should be absent.
1456        row.insert_cell_metadata(
1457            "name".to_string(),
1458            CellWriteMetadata {
1459                write_timestamp_micros: 42,
1460                expiration: None,
1461            },
1462        );
1463
1464        assert!(
1465            row.get_cell_metadata("id").is_none(),
1466            "no metadata for null column"
1467        );
1468        assert!(row.get_cell_metadata("name").is_some());
1469    }
1470
1471    /// Two SSTables with the same key: the SURVIVING (newer) cell's metadata must
1472    /// be the one carried.  This test simulates the LWW merge decision by
1473    /// constructing the two candidate rows (as would be produced by two SSTable
1474    /// reads), selecting the winner by timestamp, and asserting the winner's
1475    /// metadata is the one present.
1476    ///
1477    /// The merge itself (tombstone_merger.rs) is tested at the unit level in that
1478    /// module; here we only verify that the metadata carrier (`QueryRow`) can
1479    /// hold the winning metadata and that callers can correctly replace it.
1480    #[test]
1481    fn test_cell_metadata_lww_winner_carries_newer_timestamp() {
1482        // Older SSTable: timestamp 1_000_000 µs
1483        let mut older_row = QueryRow::new(RowKey::new(b"partition1".to_vec()));
1484        older_row.set("value".to_string(), Value::Integer(10));
1485        older_row.insert_cell_metadata(
1486            "value".to_string(),
1487            CellWriteMetadata {
1488                write_timestamp_micros: 1_000_000,
1489                expiration: None,
1490            },
1491        );
1492
1493        // Newer SSTable: timestamp 2_000_000 µs — this one wins the LWW merge.
1494        let mut newer_row = QueryRow::new(RowKey::new(b"partition1".to_vec()));
1495        newer_row.set("value".to_string(), Value::Integer(20));
1496        newer_row.insert_cell_metadata(
1497            "value".to_string(),
1498            CellWriteMetadata {
1499                write_timestamp_micros: 2_000_000,
1500                expiration: None,
1501            },
1502        );
1503
1504        // Simulate LWW merge: pick the row with the higher write timestamp.
1505        let winner = if newer_row
1506            .get_cell_metadata("value")
1507            .map(|m| m.write_timestamp_micros)
1508            .unwrap_or(0)
1509            > older_row
1510                .get_cell_metadata("value")
1511                .map(|m| m.write_timestamp_micros)
1512                .unwrap_or(0)
1513        {
1514            newer_row
1515        } else {
1516            older_row
1517        };
1518
1519        assert_eq!(
1520            winner.get("value"),
1521            Some(&Value::Integer(20)),
1522            "value from the newer SSTable must be present"
1523        );
1524        assert_eq!(
1525            winner
1526                .get_cell_metadata("value")
1527                .map(|m| m.write_timestamp_micros),
1528            Some(2_000_000),
1529            "metadata must reflect the winning (newer) cell's timestamp"
1530        );
1531    }
1532
1533    /// `set_cell_metadata` replaces the entire map atomically.
1534    #[test]
1535    fn test_set_cell_metadata_replaces_map() {
1536        let mut row = QueryRow::new(RowKey::new(vec![4]));
1537        row.insert_cell_metadata(
1538            "a".to_string(),
1539            CellWriteMetadata {
1540                write_timestamp_micros: 1,
1541                expiration: None,
1542            },
1543        );
1544
1545        let mut new_map = HashMap::new();
1546        new_map.insert(
1547            "b".to_string(),
1548            CellWriteMetadata {
1549                write_timestamp_micros: 99,
1550                expiration: None,
1551            },
1552        );
1553        row.set_cell_metadata(new_map);
1554
1555        // Old key "a" gone; new key "b" present.
1556        assert!(row.get_cell_metadata("a").is_none());
1557        assert_eq!(
1558            row.get_cell_metadata("b").map(|m| m.write_timestamp_micros),
1559            Some(99)
1560        );
1561    }
1562
1563    /// Serde round-trip: `cell_metadata` serialises and deserialises correctly.
1564    #[test]
1565    fn test_cell_metadata_serde_round_trip() {
1566        let mut row = QueryRow::new(RowKey::new(vec![5]));
1567        row.set("x".to_string(), Value::Integer(7));
1568        row.insert_cell_metadata(
1569            "x".to_string(),
1570            CellWriteMetadata {
1571                write_timestamp_micros: 123_456_789,
1572                expiration: Some(CellExpiration {
1573                    ttl_seconds: 60,
1574                    expires_at_seconds: 9999,
1575                }),
1576            },
1577        );
1578
1579        let json = serde_json::to_string(&row).expect("serialise");
1580        let back: QueryRow = serde_json::from_str(&json).expect("deserialise");
1581
1582        let meta = back
1583            .get_cell_metadata("x")
1584            .expect("metadata present after round-trip");
1585        assert_eq!(meta.write_timestamp_micros, 123_456_789);
1586        let exp = meta.expiration.as_ref().unwrap();
1587        assert_eq!(exp.ttl_seconds, 60);
1588        assert_eq!(exp.expires_at_seconds, 9999);
1589    }
1590
1591    /// When `cell_metadata` is `None`, the JSON output must not include the field
1592    /// (the `skip_serializing_if` attribute ensures backward compatibility).
1593    #[test]
1594    fn test_cell_metadata_none_omitted_from_json() {
1595        let row = QueryRow::new(RowKey::new(vec![6]));
1596        let json = serde_json::to_string(&row).expect("serialise");
1597        assert!(
1598            !json.contains("cell_metadata"),
1599            "cell_metadata must be absent from JSON when None (backward compat)"
1600        );
1601    }
1602
1603    // =========================================================================
1604    // Issue #1334: interned Arc<str> column-name keys in QueryRow.values
1605    // =========================================================================
1606
1607    /// Cells are addressable by `&str` even though the key type is now
1608    /// `Arc<str>` (via `Arc<str>: Borrow<str>`), and values are unchanged.
1609    #[test]
1610    fn test_row_values_addressable_by_str_key() {
1611        let mut row = QueryRow::new(RowKey::new(vec![1]));
1612        row.set("id", Value::Integer(7)); // &str key
1613        row.set("name".to_string(), Value::text("Zoe".to_string())); // String key
1614
1615        // `get(&str)` works unchanged.
1616        assert_eq!(row.get("id"), Some(&Value::Integer(7)));
1617        assert_eq!(row.get("name"), Some(&Value::text("Zoe".to_string())));
1618        assert_eq!(row.get("absent"), None);
1619
1620        // The key type is a shared Arc<str> handle.
1621        let key: &Arc<str> = row.values.keys().next().expect("a key");
1622        let _: &str = key; // Deref/Borrow to str compiles.
1623    }
1624
1625    /// serde round-trip of a `QueryResult` (requires serde's `rc` feature for
1626    /// the `Arc<str>` key): names and values survive and the JSON object shape
1627    /// (name→value) is unchanged.
1628    #[test]
1629    fn test_query_result_serde_round_trip_preserves_names_and_values() {
1630        let mut row = QueryRow::new(RowKey::new(vec![1]));
1631        row.set("id", Value::Integer(42));
1632        row.set("name", Value::text("Alice".to_string()));
1633        let result = QueryResult::with_rows(vec![row]);
1634
1635        // Serialize → the values object is keyed by the plain column-name string
1636        // (an Arc<str> serializes as its str, requiring serde's `rc` feature),
1637        // NOT by any Arc-wrapper shape. The per-value encoding is serde's derived
1638        // enum form (unchanged by this change); we only assert the name→value
1639        // OBJECT SHAPE here and rely on the round-trip below for value equality.
1640        let json = serde_json::to_value(&result).expect("serialise QueryResult");
1641        let values_obj = json["rows"][0]["values"]
1642            .as_object()
1643            .expect("values is a JSON object keyed by column name");
1644        assert!(values_obj.contains_key("id"), "object keyed by column name");
1645        assert!(
1646            values_obj.contains_key("name"),
1647            "object keyed by column name"
1648        );
1649
1650        // Deserialize back → identical names and values.
1651        let back: QueryResult = serde_json::from_value(json).expect("deserialise QueryResult");
1652        let r = &back.rows[0];
1653        assert_eq!(r.get("id"), Some(&Value::Integer(42)));
1654        assert_eq!(r.get("name"), Some(&Value::text("Alice".to_string())));
1655    }
1656}