Skip to main content

linkrs_query/executor/streaming/
result_utils.rs

1//! Utility functions for converting streaming results to standard formats.
2
3use std::sync::Arc;
4
5use super::chunk::DataChunk;
6use super::chunk::LocalChunkCollector;
7use super::spill::SpillManager;
8use crate::executor::base::ExecutionResult;
9use linkrs_core::error::QueryError;
10use linkrs_core::DataSet;
11
12/// Convert a Vec of DataChunks to a single DataSet
13///
14/// Merges all chunks into a unified result set with consistent column names
15/// and aggregated rows.
16///
17/// # Arguments
18/// * `chunks` - Vector of data chunks to merge
19/// * `col_names` - Optional column names to use; if None, extracted from first chunk's schema
20///
21/// # Returns
22/// * Result with merged DataSet or error if chunks are incompatible
23pub fn convert_chunks_to_dataset(
24    chunks: Vec<DataChunk>,
25    col_names: Option<Vec<String>>,
26) -> Result<DataSet, QueryError> {
27    convert_chunks_to_dataset_with_spill(chunks, col_names, None).map(|(dataset, _, _, _)| dataset)
28}
29
30/// Convert chunks to a `DataSet`, spilling through the manager when present.
31///
32/// Small results stay fully in memory; large results spill automatically once
33/// the collector threshold is exceeded. Returns the dataset plus the spilled
34/// row/byte/run counts so callers can record observability counters.
35pub fn convert_chunks_to_dataset_with_spill(
36    chunks: Vec<DataChunk>,
37    col_names: Option<Vec<String>>,
38    spill_manager: Option<Arc<SpillManager>>,
39) -> Result<(DataSet, u64, u64, u64), QueryError> {
40    if chunks.is_empty() {
41        let names = col_names.unwrap_or_default();
42        return Ok((DataSet::with_columns(names), 0, 0, 0));
43    }
44
45    let col_names = match col_names {
46        Some(names) if !names.is_empty() => names,
47        _ => chunks[0].col_names(),
48    };
49
50    let mut collector = LocalChunkCollector::new(col_names.clone());
51    if let Some(manager) = spill_manager {
52        collector.attach_spill_manager(manager);
53    }
54    let expected_cols = col_names.len();
55    for mut chunk in chunks {
56        if chunk.num_columns() != expected_cols {
57            return Err(QueryError::execution(format!(
58                "Chunk has {} columns, expected {}",
59                chunk.num_columns(),
60                expected_cols
61            )));
62        }
63        // Single terminal expansion point (selection + multiplicity aware).
64        collector.push_chunk(&mut chunk)?;
65    }
66
67    collector.finish_spill()?;
68    let spilled_rows = collector.spilled_rows();
69    let spilled_bytes = collector.spilled_bytes();
70    let spilled_runs = collector.spilled_run_count() as u64;
71    let (all_rows, _) = collector.into_rows()?;
72    Ok((
73        DataSet::from_rows(all_rows, col_names),
74        spilled_rows,
75        spilled_bytes,
76        spilled_runs,
77    ))
78}
79
80/// Convert streaming execution result to ExecutionResult
81///
82/// # Arguments
83/// * `chunks` - Result chunks from StreamingExecutionEngine
84/// * `col_names` - Optional column names override
85///
86/// # Returns
87/// * ExecutionResult with merged DataSet
88pub fn chunks_to_execution_result(
89    chunks: Vec<DataChunk>,
90    col_names: Option<Vec<String>>,
91) -> Result<ExecutionResult, QueryError> {
92    chunks_to_execution_result_with_spill(chunks, col_names, None)
93}
94
95/// Convert streaming execution result to ExecutionResult, spilling through
96/// the manager when present.
97///
98/// Large results spill automatically once the collector threshold is
99/// exceeded; callers with a spill manager should prefer this over
100/// [`chunks_to_execution_result`].
101pub fn chunks_to_execution_result_with_spill(
102    chunks: Vec<DataChunk>,
103    col_names: Option<Vec<String>>,
104    spill_manager: Option<Arc<SpillManager>>,
105) -> Result<ExecutionResult, QueryError> {
106    let (dataset, _, _, _) =
107        convert_chunks_to_dataset_with_spill(chunks, col_names, spill_manager)?;
108    Ok(ExecutionResult::DataSet { data: dataset })
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::executor::streaming::chunk::DataChunk;
115    use linkrs_core::Value;
116
117    fn create_test_chunk(rows: Vec<Vec<Value>>) -> DataChunk {
118        DataChunk::from_rows(rows)
119    }
120
121    #[test]
122    fn test_empty_chunks() {
123        let result = convert_chunks_to_dataset(vec![], None);
124        assert!(result.is_ok());
125        let ds = result.unwrap();
126        assert!(ds.is_empty());
127        assert_eq!(ds.col_count(), 0);
128    }
129
130    #[test]
131    fn test_single_chunk() {
132        let rows = vec![
133            vec![Value::Int(1), Value::string("a")],
134            vec![Value::Int(2), Value::string("b")],
135        ];
136        let chunk = create_test_chunk(rows);
137        let result = convert_chunks_to_dataset(vec![chunk], None);
138        assert!(result.is_ok());
139        let ds = result.unwrap();
140        assert_eq!(ds.row_count(), 2);
141        assert_eq!(ds.col_count(), 2);
142    }
143
144    #[test]
145    fn test_multiple_chunks() {
146        let chunk1 = create_test_chunk(vec![
147            vec![Value::Int(1), Value::string("a")],
148            vec![Value::Int(2), Value::string("b")],
149        ]);
150        let chunk2 = create_test_chunk(vec![
151            vec![Value::Int(3), Value::string("c")],
152            vec![Value::Int(4), Value::string("d")],
153        ]);
154
155        let result = convert_chunks_to_dataset(vec![chunk1, chunk2], None);
156        assert!(result.is_ok());
157        let ds = result.unwrap();
158        assert_eq!(ds.row_count(), 4);
159        assert_eq!(ds.col_count(), 2);
160    }
161
162    #[test]
163    fn test_execution_result_conversion() {
164        let chunk = create_test_chunk(vec![vec![Value::Int(42)]]);
165        let result = chunks_to_execution_result(vec![chunk], None);
166        assert!(result.is_ok());
167        if let ExecutionResult::DataSet { data: ds, .. } = result.unwrap() {
168            assert_eq!(ds.row_count(), 1);
169        } else {
170            panic!("Expected DataSet result");
171        }
172    }
173
174    #[test]
175    fn test_custom_col_names() {
176        let rows = vec![vec![Value::Int(1), Value::string("test")]];
177        let chunk = create_test_chunk(rows);
178        let col_names = vec!["id".to_string(), "name".to_string()];
179        let result = convert_chunks_to_dataset(vec![chunk], Some(col_names.clone()));
180        assert!(result.is_ok());
181        let ds = result.unwrap();
182        assert_eq!(ds.col_names, col_names);
183    }
184}