Skip to main content

scirs2_io/streaming/
relational.rs

1//! Relational streaming operations on JSON record streams.
2//!
3//! Provides memory-efficient transformations for streams of [`serde_json::Value`]
4//! records, designed for processing large datasets without loading everything
5//! into RAM.  All operators implement the [`crate::streaming::relational::StreamingOp`] trait which takes an
6//! in-memory slice (chunk) and yields a transformed `Vec<serde_json::Value>`.
7//!
8//! For truly large datasets, pair these operators with [`crate::streaming::ChunkedReader`]
9//! or iterate over JSONL files using [`crate::jsonl::JsonlReader`].
10//!
11//! # Available operators
12//!
13//! | Type | Description |
14//! |------|-------------|
15//! | [`crate::streaming::relational::StreamingFilter`] | Keep only records matching a predicate |
16//! | [`crate::streaming::relational::StreamingProject`] | Keep only specified fields |
17//! | [`crate::streaming::relational::StreamingRename`] | Rename fields |
18//! | [`crate::streaming::relational::StreamingDeduplicate`] | Remove duplicate records by key |
19//! | [`crate::streaming::relational::StreamingSort`] | External merge-sort by key |
20//! | [`crate::streaming::relational::StreamingGroupBy`] | Sort-based group-by with aggregates |
21
22use std::collections::{HashMap, HashSet};
23use std::fs;
24use std::io::{BufRead, BufReader, BufWriter, Write};
25use std::path::{Path, PathBuf};
26
27use serde_json::Value;
28
29use crate::error::{IoError, Result};
30
31// ─────────────────────────────── StreamingOp trait ───────────────────────────
32
33/// A stateless or stateful transformation on a chunk of JSON records.
34pub trait StreamingOp {
35    /// Transform a chunk of records.  Stateful operators (like Deduplicate) may
36    /// carry state across multiple calls.
37    fn process(&mut self, records: Vec<Value>) -> Vec<Value>;
38
39    /// Reset operator state (useful when reusing across multiple inputs).
40    fn reset(&mut self) {}
41}
42
43// ─────────────────────────────── StreamingFilter ─────────────────────────────
44
45/// Keep only records for which the predicate returns `true`.
46///
47/// # Example
48/// ```
49/// use scirs2_io::streaming::relational::{StreamingFilter, StreamingOp};
50/// use serde_json::json;
51///
52/// let mut f = StreamingFilter::new(|v| v["age"].as_i64().unwrap_or(0) >= 18);
53/// let out = f.process(vec![
54///     json!({"age": 17}),
55///     json!({"age": 18}),
56///     json!({"age": 25}),
57/// ]);
58/// assert_eq!(out.len(), 2);
59/// ```
60pub struct StreamingFilter {
61    predicate: Box<dyn Fn(&Value) -> bool + Send>,
62}
63
64impl StreamingFilter {
65    /// Create a new filter with the given predicate function.
66    pub fn new<F>(predicate: F) -> Self
67    where
68        F: Fn(&Value) -> bool + Send + 'static,
69    {
70        Self {
71            predicate: Box::new(predicate),
72        }
73    }
74}
75
76impl StreamingOp for StreamingFilter {
77    fn process(&mut self, records: Vec<Value>) -> Vec<Value> {
78        records
79            .into_iter()
80            .filter(|r| (self.predicate)(r))
81            .collect()
82    }
83}
84
85// ─────────────────────────────── StreamingProject ────────────────────────────
86
87/// Keep only the specified fields in each record.  Fields not present in the
88/// input record are silently omitted from the output.
89///
90/// # Example
91/// ```
92/// use scirs2_io::streaming::relational::{StreamingProject, StreamingOp};
93/// use serde_json::json;
94///
95/// let mut p = StreamingProject::new(vec!["name".into(), "age".into()]);
96/// let out = p.process(vec![json!({"name": "Alice", "age": 30, "extra": "X"})]);
97/// assert!(!out[0].as_object().unwrap().contains_key("extra"));
98/// ```
99pub struct StreamingProject {
100    fields: Vec<String>,
101}
102
103impl StreamingProject {
104    /// Create a projection keeping `fields`.
105    pub fn new(fields: Vec<String>) -> Self {
106        Self { fields }
107    }
108}
109
110impl StreamingOp for StreamingProject {
111    fn process(&mut self, records: Vec<Value>) -> Vec<Value> {
112        records
113            .into_iter()
114            .map(|r| {
115                if let Value::Object(obj) = r {
116                    let kept: serde_json::Map<String, Value> = self
117                        .fields
118                        .iter()
119                        .filter_map(|f| obj.get(f).map(|v| (f.clone(), v.clone())))
120                        .collect();
121                    Value::Object(kept)
122                } else {
123                    r
124                }
125            })
126            .collect()
127    }
128}
129
130// ─────────────────────────────── StreamingRename ─────────────────────────────
131
132/// Rename fields according to a mapping.  Fields not in the mapping are
133/// passed through unchanged.
134///
135/// # Example
136/// ```
137/// use scirs2_io::streaming::relational::{StreamingRename, StreamingOp};
138/// use serde_json::json;
139/// use std::collections::HashMap;
140///
141/// let mut renames = HashMap::new();
142/// renames.insert("old_name".into(), "new_name".into());
143/// let mut op = StreamingRename::new(renames);
144/// let out = op.process(vec![json!({"old_name": "Alice", "age": 30})]);
145/// assert!(out[0].as_object().unwrap().contains_key("new_name"));
146/// assert!(!out[0].as_object().unwrap().contains_key("old_name"));
147/// ```
148pub struct StreamingRename {
149    renames: HashMap<String, String>,
150}
151
152impl StreamingRename {
153    /// Create with a map of `old_name → new_name`.
154    pub fn new(renames: HashMap<String, String>) -> Self {
155        Self { renames }
156    }
157}
158
159impl StreamingOp for StreamingRename {
160    fn process(&mut self, records: Vec<Value>) -> Vec<Value> {
161        records
162            .into_iter()
163            .map(|r| {
164                if let Value::Object(obj) = r {
165                    let renamed: serde_json::Map<String, Value> = obj
166                        .into_iter()
167                        .map(|(k, v)| {
168                            let new_k = self.renames.get(&k).cloned().unwrap_or(k);
169                            (new_k, v)
170                        })
171                        .collect();
172                    Value::Object(renamed)
173                } else {
174                    r
175                }
176            })
177            .collect()
178    }
179}
180
181// ─────────────────────────────── StreamingDeduplicate ────────────────────────
182
183/// Remove duplicate records by a computed key.  First occurrence wins; later
184/// duplicates are dropped.  Maintains a `HashSet<String>` of seen keys in
185/// memory.
186///
187/// # Example
188/// ```
189/// use scirs2_io::streaming::relational::{StreamingDeduplicate, StreamingOp};
190/// use serde_json::json;
191///
192/// let mut dedup = StreamingDeduplicate::new(|v| {
193///     v["id"].as_i64().map(|i| i.to_string()).unwrap_or_default()
194/// });
195/// let out = dedup.process(vec![
196///     json!({"id": 1, "v": "a"}),
197///     json!({"id": 2, "v": "b"}),
198///     json!({"id": 1, "v": "c"}),  // duplicate
199/// ]);
200/// assert_eq!(out.len(), 2);
201/// ```
202pub struct StreamingDeduplicate {
203    seen: HashSet<String>,
204    key_fn: Box<dyn Fn(&Value) -> String + Send>,
205}
206
207impl StreamingDeduplicate {
208    /// Create with a key extraction function.
209    pub fn new<F>(key_fn: F) -> Self
210    where
211        F: Fn(&Value) -> String + Send + 'static,
212    {
213        Self {
214            seen: HashSet::new(),
215            key_fn: Box::new(key_fn),
216        }
217    }
218}
219
220impl StreamingOp for StreamingDeduplicate {
221    fn process(&mut self, records: Vec<Value>) -> Vec<Value> {
222        records
223            .into_iter()
224            .filter(|r| {
225                let key = (self.key_fn)(r);
226                self.seen.insert(key)
227            })
228            .collect()
229    }
230
231    fn reset(&mut self) {
232        self.seen.clear();
233    }
234}
235
236// ─────────────────────────────── StreamingSort ───────────────────────────────
237
238/// External merge-sort for large datasets that don't fit in memory.
239///
240/// Phase 1: collect `buffer_size` records at a time, sort each run, write to a
241///          temporary file.
242/// Phase 2: k-way merge of all temporary run files.
243///
244/// # Example
245/// ```no_run
246/// use scirs2_io::streaming::relational::StreamingSort;
247/// use serde_json::json;
248///
249/// let mut sorter = StreamingSort::new(
250///     16,                                              // buffer size
251///     |v| v["name"].as_str().unwrap_or("").to_string(), // sort key
252///     true,                                            // ascending
253/// );
254///
255/// let records = vec![
256///     json!({"name": "Charlie"}),
257///     json!({"name": "Alice"}),
258///     json!({"name": "Bob"}),
259/// ];
260///
261/// let sorted = sorter.sort_all(records).expect("sort");
262/// assert_eq!(sorted[0]["name"], "Alice");
263/// ```
264pub struct StreamingSort {
265    buffer_size: usize,
266    key_fn: Box<dyn Fn(&Value) -> String + Send>,
267    ascending: bool,
268}
269
270impl StreamingSort {
271    /// Create a streaming sorter.
272    ///
273    /// - `buffer_size`: number of records to buffer per run before writing to disk.
274    /// - `key_fn`: extract the sort key from each record.
275    /// - `ascending`: sort direction.
276    pub fn new<F>(buffer_size: usize, key_fn: F, ascending: bool) -> Self
277    where
278        F: Fn(&Value) -> String + Send + 'static,
279    {
280        Self {
281            buffer_size: buffer_size.max(1),
282            key_fn: Box::new(key_fn),
283            ascending,
284        }
285    }
286
287    /// Sort all records using external merge-sort.
288    ///
289    /// For datasets small enough to fit in `buffer_size`, this is equivalent to
290    /// an in-memory sort.
291    pub fn sort_all(&mut self, records: Vec<Value>) -> Result<Vec<Value>> {
292        if records.len() <= self.buffer_size {
293            return Ok(self.sort_in_memory(records));
294        }
295        // Phase 1: produce sorted runs on disk
296        let run_files = self.produce_runs(&records)?;
297        // Phase 2: k-way merge
298        let merged = self.kway_merge(run_files)?;
299        Ok(merged)
300    }
301
302    fn sort_in_memory(&self, mut records: Vec<Value>) -> Vec<Value> {
303        records.sort_by(|a, b| {
304            let ka = (self.key_fn)(a);
305            let kb = (self.key_fn)(b);
306            if self.ascending {
307                ka.cmp(&kb)
308            } else {
309                kb.cmp(&ka)
310            }
311        });
312        records
313    }
314
315    fn produce_runs(&self, records: &[Value]) -> Result<Vec<PathBuf>> {
316        let mut paths = Vec::new();
317        let temp_dir = std::env::temp_dir();
318
319        for (run_idx, chunk) in records.chunks(self.buffer_size).enumerate() {
320            let mut run: Vec<Value> = chunk.to_vec();
321            run.sort_by(|a, b| {
322                let ka = (self.key_fn)(a);
323                let kb = (self.key_fn)(b);
324                if self.ascending {
325                    ka.cmp(&kb)
326                } else {
327                    kb.cmp(&ka)
328                }
329            });
330
331            let path = temp_dir.join(format!(
332                "scirs2_sort_run_{run_idx}_{}.jsonl",
333                std::process::id()
334            ));
335            let f = fs::File::create(&path).map_err(IoError::Io)?;
336            let mut w = BufWriter::new(f);
337            for record in &run {
338                let line = serde_json::to_string(record)
339                    .map_err(|e| IoError::SerializationError(e.to_string()))?;
340                writeln!(w, "{line}").map_err(IoError::Io)?;
341            }
342            w.flush().map_err(IoError::Io)?;
343            paths.push(path);
344        }
345        Ok(paths)
346    }
347
348    fn kway_merge(&self, run_files: Vec<PathBuf>) -> Result<Vec<Value>> {
349        // Open all run files
350        let mut readers: Vec<BufReader<fs::File>> = run_files
351            .iter()
352            .map(|p| fs::File::open(p).map(BufReader::new).map_err(IoError::Io))
353            .collect::<Result<Vec<_>>>()?;
354
355        // Prime each reader with the first record, computing the sort key
356        // using the same key_fn used for run-sorting
357        let mut heads: Vec<Option<(String, Value)>> = readers
358            .iter_mut()
359            .map(|r| peek_next_json_with_key(r, &self.key_fn))
360            .collect::<Result<Vec<_>>>()?;
361
362        let mut merged = Vec::new();
363
364        loop {
365            // Find the run with the minimum (or maximum) key
366            let best_idx = heads
367                .iter()
368                .enumerate()
369                .filter_map(|(i, h)| h.as_ref().map(|(k, _)| (i, k.clone())))
370                .min_by(|(_, ka), (_, kb)| {
371                    if self.ascending {
372                        ka.cmp(kb)
373                    } else {
374                        kb.cmp(ka)
375                    }
376                })
377                .map(|(i, _)| i);
378
379            match best_idx {
380                None => break,
381                Some(i) => {
382                    if let Some((_, value)) = heads[i].take() {
383                        merged.push(value);
384                    }
385                    heads[i] = peek_next_json_with_key(&mut readers[i], &self.key_fn)?;
386                }
387            }
388        }
389
390        // Clean up temp files
391        for path in &run_files {
392            let _ = fs::remove_file(path);
393        }
394
395        Ok(merged)
396    }
397}
398
399/// Read the next JSON record from a reader, computing the sort key via `key_fn`.
400fn peek_next_json_with_key<R: BufRead>(
401    reader: &mut R,
402    key_fn: &dyn Fn(&Value) -> String,
403) -> Result<Option<(String, Value)>> {
404    let mut line = String::new();
405    match reader.read_line(&mut line) {
406        Ok(0) => Ok(None),
407        Ok(_) => {
408            let trimmed = line.trim_end();
409            if trimmed.is_empty() {
410                return Ok(None);
411            }
412            let val: Value = serde_json::from_str(trimmed)
413                .map_err(|e| IoError::DeserializationError(e.to_string()))?;
414            let key = key_fn(&val);
415            Ok(Some((key, val)))
416        }
417        Err(e) => Err(IoError::Io(e)),
418    }
419}
420
421// ─────────────────────────────── StreamingAggregate ──────────────────────────
422
423/// A stateful streaming aggregate that can be updated with new records and
424/// queried for the current result.
425pub trait StreamingAggregate: Send {
426    /// Update the aggregate with one record.
427    fn update(&mut self, record: &Value);
428    /// Return the current aggregate result as a JSON value.
429    fn result(&self) -> Value;
430    /// Reset the aggregate to its initial state.
431    fn reset_agg(&mut self);
432    /// Name of this aggregate (used as output field name).
433    fn name(&self) -> &str;
434}
435
436/// Count aggregate: counts the number of records seen.
437pub struct CountAggregate {
438    count: u64,
439    field_name: String,
440}
441
442impl CountAggregate {
443    /// Create a count aggregate with the given output field name.
444    pub fn new(field_name: impl Into<String>) -> Self {
445        Self {
446            count: 0,
447            field_name: field_name.into(),
448        }
449    }
450}
451
452impl StreamingAggregate for CountAggregate {
453    fn update(&mut self, _record: &Value) {
454        self.count += 1;
455    }
456    fn result(&self) -> Value {
457        Value::Number(self.count.into())
458    }
459    fn reset_agg(&mut self) {
460        self.count = 0;
461    }
462    fn name(&self) -> &str {
463        &self.field_name
464    }
465}
466
467/// Sum aggregate: sums a numeric field.
468pub struct SumAggregate {
469    field: String,
470    sum: f64,
471    output_name: String,
472}
473
474impl SumAggregate {
475    /// Sum the numeric field `field`, output as `output_name`.
476    pub fn new(field: impl Into<String>, output_name: impl Into<String>) -> Self {
477        Self {
478            field: field.into(),
479            sum: 0.0,
480            output_name: output_name.into(),
481        }
482    }
483}
484
485impl StreamingAggregate for SumAggregate {
486    fn update(&mut self, record: &Value) {
487        if let Some(n) = record.get(&self.field).and_then(|v| v.as_f64()) {
488            self.sum += n;
489        }
490    }
491    fn result(&self) -> Value {
492        serde_json::json!(self.sum)
493    }
494    fn reset_agg(&mut self) {
495        self.sum = 0.0;
496    }
497    fn name(&self) -> &str {
498        &self.output_name
499    }
500}
501
502/// Min aggregate: tracks the minimum value of a numeric field.
503pub struct MinAggregate {
504    field: String,
505    min: f64,
506    output_name: String,
507    has_value: bool,
508}
509
510impl MinAggregate {
511    /// Track minimum of `field`, output as `output_name`.
512    pub fn new(field: impl Into<String>, output_name: impl Into<String>) -> Self {
513        Self {
514            field: field.into(),
515            min: f64::MAX,
516            output_name: output_name.into(),
517            has_value: false,
518        }
519    }
520}
521
522impl StreamingAggregate for MinAggregate {
523    fn update(&mut self, record: &Value) {
524        if let Some(n) = record.get(&self.field).and_then(|v| v.as_f64()) {
525            if !self.has_value || n < self.min {
526                self.min = n;
527                self.has_value = true;
528            }
529        }
530    }
531    fn result(&self) -> Value {
532        if self.has_value {
533            serde_json::json!(self.min)
534        } else {
535            Value::Null
536        }
537    }
538    fn reset_agg(&mut self) {
539        self.min = f64::MAX;
540        self.has_value = false;
541    }
542    fn name(&self) -> &str {
543        &self.output_name
544    }
545}
546
547/// Max aggregate: tracks the maximum value of a numeric field.
548pub struct MaxAggregate {
549    field: String,
550    max: f64,
551    output_name: String,
552    has_value: bool,
553}
554
555impl MaxAggregate {
556    /// Track maximum of `field`, output as `output_name`.
557    pub fn new(field: impl Into<String>, output_name: impl Into<String>) -> Self {
558        Self {
559            field: field.into(),
560            max: f64::MIN,
561            output_name: output_name.into(),
562            has_value: false,
563        }
564    }
565}
566
567impl StreamingAggregate for MaxAggregate {
568    fn update(&mut self, record: &Value) {
569        if let Some(n) = record.get(&self.field).and_then(|v| v.as_f64()) {
570            if !self.has_value || n > self.max {
571                self.max = n;
572                self.has_value = true;
573            }
574        }
575    }
576    fn result(&self) -> Value {
577        if self.has_value {
578            serde_json::json!(self.max)
579        } else {
580            Value::Null
581        }
582    }
583    fn reset_agg(&mut self) {
584        self.max = f64::MIN;
585        self.has_value = false;
586    }
587    fn name(&self) -> &str {
588        &self.output_name
589    }
590}
591
592// ─────────────────────────────── StreamingGroupBy ────────────────────────────
593
594/// Sort-based streaming group-by with arbitrary aggregates.
595///
596/// Processing strategy:
597/// 1. Sort records by key using [`StreamingSort`].
598/// 2. Iterate through sorted records, accumulating aggregates per group.
599/// 3. Emit one output record per group.
600///
601/// # Example
602/// ```
603/// use scirs2_io::streaming::relational::{
604///     StreamingGroupBy, SumAggregate, CountAggregate,
605/// };
606/// use serde_json::json;
607///
608/// let records = vec![
609///     json!({"dept": "eng",  "salary": 100.0}),
610///     json!({"dept": "hr",   "salary": 80.0}),
611///     json!({"dept": "eng",  "salary": 120.0}),
612///     json!({"dept": "hr",   "salary": 90.0}),
613/// ];
614///
615/// let aggs: Vec<Box<dyn scirs2_io::streaming::relational::StreamingAggregate>> = vec![
616///     Box::new(CountAggregate::new("count")),
617///     Box::new(SumAggregate::new("salary", "total_salary")),
618/// ];
619///
620/// let mut gb = StreamingGroupBy::new(
621///     |v| v["dept"].as_str().unwrap_or("").to_string(),
622///     aggs,
623///     64,
624/// );
625///
626/// let result = gb.run(records).expect("group by");
627/// assert_eq!(result.len(), 2); // two departments
628/// ```
629pub struct StreamingGroupBy {
630    key_fn: Box<dyn Fn(&Value) -> String + Send>,
631    aggregates: Vec<Box<dyn StreamingAggregate>>,
632    buffer_size: usize,
633}
634
635impl StreamingGroupBy {
636    /// Create a group-by operator.
637    ///
638    /// - `key_fn`: extract the group key.
639    /// - `aggregates`: list of aggregate functions.
640    /// - `buffer_size`: run-sort buffer size passed to [`StreamingSort`].
641    pub fn new<F>(
642        key_fn: F,
643        aggregates: Vec<Box<dyn StreamingAggregate>>,
644        buffer_size: usize,
645    ) -> Self
646    where
647        F: Fn(&Value) -> String + Send + 'static,
648    {
649        Self {
650            key_fn: Box::new(key_fn),
651            aggregates,
652            buffer_size,
653        }
654    }
655
656    /// Execute the group-by over `records`.
657    ///
658    /// Records are sorted by key, then groups are processed sequentially.
659    pub fn run(&mut self, records: Vec<Value>) -> Result<Vec<Value>> {
660        // Sort by key
661        let key_fn_clone = &self.key_fn;
662        let mut sorted = records;
663        sorted.sort_by(|a, b| {
664            let ka = (key_fn_clone)(a);
665            let kb = (key_fn_clone)(b);
666            ka.cmp(&kb)
667        });
668
669        if sorted.is_empty() {
670            return Ok(Vec::new());
671        }
672
673        let mut output = Vec::new();
674        let mut current_key = (self.key_fn)(&sorted[0]);
675        for agg in &mut self.aggregates {
676            agg.reset_agg();
677        }
678
679        for record in sorted {
680            let key = (self.key_fn)(&record);
681            if key != current_key {
682                // Emit group
683                output.push(self.emit_group(&current_key));
684                current_key = key;
685                for agg in &mut self.aggregates {
686                    agg.reset_agg();
687                }
688            }
689            for agg in &mut self.aggregates {
690                agg.update(&record);
691            }
692        }
693        // Emit final group
694        output.push(self.emit_group(&current_key));
695        Ok(output)
696    }
697
698    fn emit_group(&self, key: &str) -> Value {
699        let mut obj = serde_json::Map::new();
700        obj.insert("_key".to_string(), Value::String(key.to_owned()));
701        for agg in &self.aggregates {
702            obj.insert(agg.name().to_string(), agg.result());
703        }
704        Value::Object(obj)
705    }
706}
707
708// ─────────────────────────────── Pipeline ────────────────────────────────────
709
710/// Chain of [`StreamingOp`] operators applied sequentially.
711///
712/// Records flow through each operator in order.  Useful for building
713/// composable ETL pipelines.
714///
715/// # Example
716/// ```
717/// use scirs2_io::streaming::relational::{
718///     StreamingPipeline, StreamingFilter, StreamingProject, StreamingOp,
719/// };
720/// use serde_json::json;
721///
722/// let mut pipeline = StreamingPipeline::new();
723/// pipeline.add(StreamingFilter::new(|v| v["active"].as_bool().unwrap_or(false)));
724/// pipeline.add(StreamingProject::new(vec!["name".into()]));
725///
726/// let out = pipeline.run(vec![
727///     json!({"name": "Alice", "active": true,  "score": 99}),
728///     json!({"name": "Bob",   "active": false, "score": 80}),
729/// ]);
730/// assert_eq!(out.len(), 1);
731/// assert!(out[0].as_object().unwrap().contains_key("name"));
732/// assert!(!out[0].as_object().unwrap().contains_key("score"));
733/// ```
734pub struct StreamingPipeline {
735    ops: Vec<Box<dyn StreamingOp>>,
736}
737
738impl StreamingPipeline {
739    /// Create an empty pipeline.
740    pub fn new() -> Self {
741        Self { ops: Vec::new() }
742    }
743
744    /// Append an operator to the pipeline.
745    pub fn add<O: StreamingOp + 'static>(&mut self, op: O) {
746        self.ops.push(Box::new(op));
747    }
748
749    /// Run all operators in sequence.
750    pub fn run(&mut self, records: Vec<Value>) -> Vec<Value> {
751        let mut current = records;
752        for op in &mut self.ops {
753            current = op.process(current);
754        }
755        current
756    }
757}
758
759impl Default for StreamingPipeline {
760    fn default() -> Self {
761        Self::new()
762    }
763}
764
765// ─────────────────────────────── File-based helpers ──────────────────────────
766
767/// Sort a JSONL file using external merge-sort, writing output to another file.
768///
769/// Useful for command-line-style sorting of large JSONL datasets.
770pub fn sort_jsonl_file<F>(
771    input: &Path,
772    output: &Path,
773    key_fn: F,
774    ascending: bool,
775    buffer_size: usize,
776) -> Result<usize>
777where
778    F: Fn(&Value) -> String + Send + 'static,
779{
780    let f = fs::File::open(input).map_err(IoError::Io)?;
781    let reader = BufReader::new(f);
782    let mut records = Vec::new();
783    for line in reader.lines() {
784        let line = line.map_err(IoError::Io)?;
785        let trimmed = line.trim();
786        if trimmed.is_empty() {
787            continue;
788        }
789        let val: Value = serde_json::from_str(trimmed)
790            .map_err(|e| IoError::DeserializationError(e.to_string()))?;
791        records.push(val);
792    }
793
794    let total = records.len();
795    let mut sorter = StreamingSort::new(buffer_size, key_fn, ascending);
796    let sorted = sorter.sort_all(records)?;
797
798    let out_f = fs::File::create(output).map_err(IoError::Io)?;
799    let mut w = BufWriter::new(out_f);
800    for record in &sorted {
801        let line = serde_json::to_string(record)
802            .map_err(|e| IoError::SerializationError(e.to_string()))?;
803        writeln!(w, "{line}").map_err(IoError::Io)?;
804    }
805    w.flush().map_err(IoError::Io)?;
806    Ok(total)
807}
808
809// ─────────────────────────────── Tests ───────────────────────────────────────
810
811#[cfg(test)]
812mod tests {
813    use super::*;
814    use serde_json::json;
815    use std::env::temp_dir;
816
817    fn records() -> Vec<Value> {
818        vec![
819            json!({"id": 1, "dept": "eng",  "salary": 100.0, "name": "Alice", "active": true}),
820            json!({"id": 2, "dept": "hr",   "salary": 80.0,  "name": "Bob",   "active": false}),
821            json!({"id": 3, "dept": "eng",  "salary": 120.0, "name": "Charlie","active": true}),
822            json!({"id": 4, "dept": "hr",   "salary": 90.0,  "name": "Dave",  "active": true}),
823            json!({"id": 5, "dept": "eng",  "salary": 110.0, "name": "Eve",   "active": false}),
824        ]
825    }
826
827    #[test]
828    fn test_filter() {
829        let mut f = StreamingFilter::new(|v| v["active"].as_bool().unwrap_or(false));
830        let out = f.process(records());
831        assert_eq!(out.len(), 3);
832    }
833
834    #[test]
835    fn test_project() {
836        let mut p = StreamingProject::new(vec!["id".into(), "name".into()]);
837        let out = p.process(records());
838        assert_eq!(out.len(), 5);
839        let obj = out[0].as_object().expect("object");
840        assert!(obj.contains_key("id"));
841        assert!(obj.contains_key("name"));
842        assert!(!obj.contains_key("salary"));
843        assert!(!obj.contains_key("dept"));
844    }
845
846    #[test]
847    fn test_rename() {
848        let mut renames = HashMap::new();
849        renames.insert("salary".into(), "pay".into());
850        let mut op = StreamingRename::new(renames);
851        let out = op.process(records());
852        let obj = out[0].as_object().expect("object");
853        assert!(obj.contains_key("pay"));
854        assert!(!obj.contains_key("salary"));
855    }
856
857    #[test]
858    fn test_deduplicate() {
859        let input = vec![
860            json!({"id": 1, "v": "a"}),
861            json!({"id": 2, "v": "b"}),
862            json!({"id": 1, "v": "c"}), // duplicate
863            json!({"id": 3, "v": "d"}),
864        ];
865        let mut dedup = StreamingDeduplicate::new(|v| {
866            v["id"].as_i64().map(|i| i.to_string()).unwrap_or_default()
867        });
868        let out = dedup.process(input);
869        assert_eq!(out.len(), 3);
870        assert_eq!(out[0]["v"], "a"); // first occurrence kept
871    }
872
873    #[test]
874    fn test_sort_ascending() {
875        let input = vec![
876            json!({"name": "Charlie"}),
877            json!({"name": "Alice"}),
878            json!({"name": "Bob"}),
879        ];
880        let mut sorter =
881            StreamingSort::new(16, |v| v["name"].as_str().unwrap_or("").to_string(), true);
882        let out = sorter.sort_all(input).expect("sort");
883        assert_eq!(out[0]["name"], "Alice");
884        assert_eq!(out[1]["name"], "Bob");
885        assert_eq!(out[2]["name"], "Charlie");
886    }
887
888    #[test]
889    fn test_sort_descending() {
890        let input = vec![
891            json!({"name": "Alice"}),
892            json!({"name": "Charlie"}),
893            json!({"name": "Bob"}),
894        ];
895        let mut sorter =
896            StreamingSort::new(16, |v| v["name"].as_str().unwrap_or("").to_string(), false);
897        let out = sorter.sort_all(input).expect("sort");
898        assert_eq!(out[0]["name"], "Charlie");
899        assert_eq!(out[1]["name"], "Bob");
900        assert_eq!(out[2]["name"], "Alice");
901    }
902
903    #[test]
904    fn test_sort_external_merge() {
905        // Force external merge by using a small buffer
906        let input: Vec<Value> = (0..20_i64).rev().map(|i| json!({"v": i})).collect();
907        let mut sorter = StreamingSort::new(
908            3, // tiny buffer → many run files
909            |v| {
910                let n = v["v"].as_i64().unwrap_or(0);
911                format!("{n:010}") // zero-padded for string sort
912            },
913            true,
914        );
915        let out = sorter.sort_all(input).expect("sort");
916        assert_eq!(out.len(), 20);
917        assert_eq!(out[0]["v"], 0);
918        assert_eq!(out[19]["v"], 19);
919    }
920
921    #[test]
922    fn test_group_by_count_sum() {
923        let recs = records();
924        let aggs: Vec<Box<dyn StreamingAggregate>> = vec![
925            Box::new(CountAggregate::new("count")),
926            Box::new(SumAggregate::new("salary", "total")),
927        ];
928        let mut gb =
929            StreamingGroupBy::new(|v| v["dept"].as_str().unwrap_or("").to_string(), aggs, 64);
930        let out = gb.run(recs).expect("group by");
931        assert_eq!(out.len(), 2);
932
933        // Find the "eng" group
934        let eng = out.iter().find(|v| v["_key"] == "eng").expect("eng group");
935        assert_eq!(eng["count"], 3);
936        assert!((eng["total"].as_f64().unwrap() - 330.0).abs() < 1e-9);
937    }
938
939    #[test]
940    fn test_group_by_min_max() {
941        let recs = records();
942        let aggs: Vec<Box<dyn StreamingAggregate>> = vec![
943            Box::new(MinAggregate::new("salary", "min_sal")),
944            Box::new(MaxAggregate::new("salary", "max_sal")),
945        ];
946        let mut gb =
947            StreamingGroupBy::new(|v| v["dept"].as_str().unwrap_or("").to_string(), aggs, 64);
948        let out = gb.run(recs).expect("group by");
949        let eng = out.iter().find(|v| v["_key"] == "eng").expect("eng group");
950        assert!((eng["min_sal"].as_f64().unwrap() - 100.0).abs() < 1e-9);
951        assert!((eng["max_sal"].as_f64().unwrap() - 120.0).abs() < 1e-9);
952    }
953
954    #[test]
955    fn test_pipeline() {
956        let mut pipeline = StreamingPipeline::new();
957        pipeline.add(StreamingFilter::new(|v| {
958            v["active"].as_bool().unwrap_or(false)
959        }));
960        pipeline.add(StreamingProject::new(vec!["id".into(), "name".into()]));
961
962        let out = pipeline.run(records());
963        assert_eq!(out.len(), 3);
964        let obj = out[0].as_object().expect("object");
965        assert!(obj.contains_key("name"));
966        assert!(!obj.contains_key("salary"));
967    }
968
969    #[test]
970    fn test_sort_jsonl_file() {
971        let temp = temp_dir();
972        let input_path = temp.join("sort_test_input.jsonl");
973        let output_path = temp.join("sort_test_output.jsonl");
974
975        let lines = [r#"{"v": 3}"#, r#"{"v": 1}"#, r#"{"v": 2}"#];
976        fs::write(&input_path, lines.join("\n")).expect("write");
977
978        let count = sort_jsonl_file(
979            &input_path,
980            &output_path,
981            |v| {
982                let n = v["v"].as_i64().unwrap_or(0);
983                format!("{n:010}")
984            },
985            true,
986            100,
987        )
988        .expect("sort file");
989
990        assert_eq!(count, 3);
991
992        let output = fs::read_to_string(&output_path).expect("read");
993        let vals: Vec<i64> = output
994            .lines()
995            .map(|l| {
996                serde_json::from_str::<Value>(l).expect("json")["v"]
997                    .as_i64()
998                    .unwrap()
999            })
1000            .collect();
1001        assert_eq!(vals, vec![1, 2, 3]);
1002    }
1003
1004    #[test]
1005    fn test_deduplicate_reset() {
1006        let mut dedup = StreamingDeduplicate::new(|v| {
1007            v["id"].as_i64().map(|i| i.to_string()).unwrap_or_default()
1008        });
1009        let first = dedup.process(vec![json!({"id": 1}), json!({"id": 1})]);
1010        assert_eq!(first.len(), 1);
1011
1012        dedup.reset(); // clear seen set
1013        let second = dedup.process(vec![json!({"id": 1}), json!({"id": 1})]);
1014        assert_eq!(second.len(), 1);
1015    }
1016}