1use crate::{CanonicalValue, Transforms};
2use std::error::Error;
3use std::io::{BufRead, BufReader, Read, Write};
4
5const SCHEMA_SAMPLE_ROWS: usize = 10_000;
6
7pub fn stream_ndjson_to_delimited(
28 reader: impl Read,
29 writer: impl Write,
30 to_delim: u8,
31 transforms: &Transforms,
32) -> Result<(), Box<dyn Error>> {
33 let mut buf = BufReader::new(reader);
34 let mut headers: Vec<String> = Vec::new();
35 let mut sample: Vec<CanonicalValue> = Vec::new();
36 let mut line = String::new();
37
38 loop {
39 line.clear();
40 if buf.read_line(&mut line)? == 0 {
41 break;
42 }
43 let trimmed = line.trim();
44 if trimmed.is_empty() {
45 continue;
46 }
47 let v: serde_json::Value = serde_json::from_str(trimmed)?;
48 let canon = CanonicalValue::from_json_value(v);
49 if let CanonicalValue::Object(o) = &canon {
50 for (k, _) in o {
51 if !headers.contains(k) {
52 headers.push(k.clone());
53 }
54 }
55 }
56 sample.push(canon);
57 if sample.len() >= SCHEMA_SAMPLE_ROWS {
58 break;
59 }
60 }
61
62 let mut renamed = headers.clone();
63 for (from, to) in &transforms.rename {
64 for h in renamed.iter_mut() {
65 if h == from {
66 *h = to.clone();
67 break;
68 }
69 }
70 }
71 let final_headers: Vec<String> = match &transforms.select {
72 Some(sel) => sel.clone(),
73 None => renamed,
74 };
75
76 let mut wtr = csv::WriterBuilder::new()
77 .delimiter(to_delim)
78 .from_writer(writer);
79 wtr.write_record(&final_headers)?;
80
81 let limit = transforms.limit.unwrap_or(usize::MAX);
82 let mut emitted = 0usize;
83 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
84 let dedup_cols = transforms.dedup.clone();
85
86 for canon in sample {
87 if emitted >= limit {
88 break;
89 }
90 if let CanonicalValue::Object(row) = canon {
91 if let Some(new_row) = transforms.apply_to_row(row) {
92 write_row(
93 &mut wtr,
94 &new_row,
95 &final_headers,
96 &dedup_cols,
97 &mut seen,
98 &mut emitted,
99 )?;
100 }
101 }
102 }
103
104 while emitted < limit {
105 line.clear();
106 if buf.read_line(&mut line)? == 0 {
107 break;
108 }
109 let trimmed = line.trim();
110 if trimmed.is_empty() {
111 continue;
112 }
113 let v: serde_json::Value = serde_json::from_str(trimmed)?;
114 if let CanonicalValue::Object(row) = CanonicalValue::from_json_value(v) {
115 if let Some(new_row) = transforms.apply_to_row(row) {
116 write_row(
117 &mut wtr,
118 &new_row,
119 &final_headers,
120 &dedup_cols,
121 &mut seen,
122 &mut emitted,
123 )?;
124 }
125 }
126 }
127
128 wtr.flush()?;
129 Ok(())
130}
131
132pub fn stream_delimited_to_ndjson(
148 reader: impl Read,
149 from_delim: u8,
150 writer: impl Write,
151 transforms: &Transforms,
152) -> Result<(), Box<dyn Error>> {
153 let mut rdr = csv::ReaderBuilder::new()
154 .delimiter(from_delim)
155 .from_reader(reader);
156 let raw_headers: Vec<String> = rdr.headers()?.iter().map(|s| s.to_string()).collect();
157 let mut out = std::io::BufWriter::new(writer);
158 let limit = transforms.limit.unwrap_or(usize::MAX);
159 let mut emitted = 0usize;
160 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
161 let dedup_cols = transforms.dedup.clone();
162
163 for result in rdr.records() {
164 if emitted >= limit {
165 break;
166 }
167 let record = result?;
168 let mut row: Vec<(String, CanonicalValue)> = Vec::with_capacity(raw_headers.len());
169 for (i, h) in raw_headers.iter().enumerate() {
170 row.push((
171 h.clone(),
172 CanonicalValue::String(record.get(i).unwrap_or("").to_string()),
173 ));
174 }
175 let new_row = match transforms.apply_to_row(row) {
176 Some(r) => r,
177 None => continue,
178 };
179 if let Some(cols) = &dedup_cols {
180 let key = dedup_key(&new_row, cols);
181 if !seen.insert(key) {
182 continue;
183 }
184 }
185 let obj = CanonicalValue::Object(new_row);
186 let json = serde_json::to_string(&obj.to_json_value())?;
187 writeln!(out, "{json}")?;
188 emitted += 1;
189 }
190 out.flush()?;
191 Ok(())
192}
193
194pub fn stream_ndjson_to_ndjson(
211 reader: impl Read,
212 writer: impl Write,
213 transforms: &Transforms,
214) -> Result<(), Box<dyn Error>> {
215 let buf = BufReader::new(reader);
216 let mut out = std::io::BufWriter::new(writer);
217 let limit = transforms.limit.unwrap_or(usize::MAX);
218 let mut emitted = 0usize;
219 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
220 let dedup_cols = transforms.dedup.clone();
221
222 for line in buf.lines() {
223 if emitted >= limit {
224 break;
225 }
226 let line = line?;
227 let trimmed = line.trim();
228 if trimmed.is_empty() {
229 continue;
230 }
231 let v: serde_json::Value = serde_json::from_str(trimmed)?;
232 let row = match CanonicalValue::from_json_value(v) {
233 CanonicalValue::Object(o) => o,
234 _ => continue,
235 };
236 let new_row = match transforms.apply_to_row(row) {
237 Some(r) => r,
238 None => continue,
239 };
240 if let Some(cols) = &dedup_cols {
241 let key = dedup_key(&new_row, cols);
242 if !seen.insert(key) {
243 continue;
244 }
245 }
246 let obj = CanonicalValue::Object(new_row);
247 let json = serde_json::to_string(&obj.to_json_value())?;
248 writeln!(out, "{json}")?;
249 emitted += 1;
250 }
251 out.flush()?;
252 Ok(())
253}
254
255fn write_row<W: Write>(
256 wtr: &mut csv::Writer<W>,
257 row: &[(String, CanonicalValue)],
258 final_headers: &[String],
259 dedup_cols: &Option<Vec<String>>,
260 seen: &mut std::collections::HashSet<String>,
261 emitted: &mut usize,
262) -> Result<(), Box<dyn Error>> {
263 if let Some(cols) = dedup_cols {
264 let key = dedup_key(row, cols);
265 if !seen.insert(key) {
266 return Ok(());
267 }
268 }
269 let values: Vec<String> = final_headers
270 .iter()
271 .map(|h| {
272 row.iter()
273 .find(|(k, _)| k == h)
274 .map(|(_, v)| canon_str(v))
275 .unwrap_or_default()
276 })
277 .collect();
278 wtr.write_record(&values)?;
279 *emitted += 1;
280 Ok(())
281}
282
283fn dedup_key(row: &[(String, CanonicalValue)], cols: &[String]) -> String {
284 if cols.is_empty() {
285 row.iter()
286 .map(|(_, v)| canon_str(v))
287 .collect::<Vec<_>>()
288 .join("\x1f")
289 } else {
290 cols.iter()
291 .map(|c| {
292 row.iter()
293 .find(|(k, _)| k == c)
294 .map(|(_, v)| canon_str(v))
295 .unwrap_or_default()
296 })
297 .collect::<Vec<_>>()
298 .join("\x1f")
299 }
300}
301
302fn canon_str(v: &CanonicalValue) -> String {
303 match v {
304 CanonicalValue::Null => String::new(),
305 CanonicalValue::Bool(b) => b.to_string(),
306 CanonicalValue::Number(n) => n.to_string(),
307 CanonicalValue::String(s) => s.clone(),
308 CanonicalValue::Bytes(b) => {
309 use base64::Engine;
310 base64::engine::general_purpose::STANDARD.encode(b)
311 }
312 _ => String::new(),
313 }
314}