1pub mod avro_types;
9pub(crate) mod export_keys;
10pub mod export_modal;
11pub mod nested_json;
12pub mod output_file;
13pub mod python_script;
14
15use std::io::{self, BufWriter, Write};
16use std::path::PathBuf;
17use std::time::{Duration, Instant};
18
19use color_eyre::Result;
20use polars::prelude::{
21 CsvWriter, DataFrame, IpcWriter, JsonFormat, JsonWriter, LazyFrame, ParquetWriter, SerWriter,
22};
23
24use crate::CompressionFormat;
25use crate::export::export_modal::ExportFormat;
26use crate::export::output_file::{OutputFile, Overwrite};
27
28#[derive(Debug, Clone)]
29pub struct ExportOptions {
30 pub csv_delimiter: u8,
31 pub csv_include_header: bool,
32 pub source_file: bool,
35 pub csv_compression: Option<CompressionFormat>,
36 pub json_compression: Option<CompressionFormat>,
37 pub ndjson_compression: Option<CompressionFormat>,
38}
39
40impl ExportOptions {
41 pub fn compression(&self, format: ExportFormat) -> Option<CompressionFormat> {
43 match format {
44 ExportFormat::Csv | ExportFormat::Tsv | ExportFormat::Psv => self.csv_compression,
45 ExportFormat::Json => self.json_compression,
46 ExportFormat::Ndjson => self.ndjson_compression,
47 ExportFormat::Parquet | ExportFormat::Ipc | ExportFormat::Avro => None,
48 }
49 }
50
51 pub fn delimiter(&self, format: ExportFormat) -> u8 {
53 format.preset_delimiter().unwrap_or(self.csv_delimiter)
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct ExportRequest {
60 pub path: PathBuf,
61 pub format: ExportFormat,
62 pub options: ExportOptions,
63 pub overwrite: Overwrite,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum Route {
69 Streamed,
71 Collected,
73}
74
75impl ExportRequest {
76 pub fn route(&self, polars_streaming: bool) -> Route {
79 let sinkable = (self.format.is_delimited() || self.format == ExportFormat::Parquet)
80 && self.options.compression(self.format).is_none();
81 if cfg!(feature = "streaming") && polars_streaming && sinkable {
82 Route::Streamed
83 } else {
84 Route::Collected
85 }
86 }
87}
88
89const PROGRESS_EVERY: Duration = Duration::from_millis(250);
91
92pub fn run(
96 lf: LazyFrame,
97 request: &ExportRequest,
98 polars_streaming: bool,
99 mut written: impl FnMut(u64) + Send + 'static,
100) -> Result<()> {
101 let lf = request.format.prepare(lf)?;
102 let polars_streaming = crate::analysis::statistics::may_stream(&lf, polars_streaming);
103 let mut out = OutputFile::create(&request.path, request.overwrite)?;
105 match request.route(polars_streaming) {
106 #[cfg(feature = "streaming")]
107 Route::Streamed => {
108 written(0);
109 let file = Counted::new(out.file().try_clone()?, written);
110 sink(lf, request.format, &request.options, file.into_writable())?;
111 }
112 _ => {
113 let mut df = crate::analysis::statistics::collect_lazy(lf, polars_streaming)?;
114 written(0);
115 let file = Counted::new(out.file(), written);
116 encode(&mut df, request.format, &request.options, file)?;
117 }
118 }
119 out.commit()?;
120 Ok(())
121}
122
123#[cfg(feature = "streaming")]
127fn sink(
128 lf: LazyFrame,
129 format: ExportFormat,
130 options: &ExportOptions,
131 writable: polars::io::utils::file::Writable,
132) -> Result<()> {
133 use polars::prelude::{
134 CompatLevel, CsvWriterOptions, Engine, FileWriteFormat, ParquetWriteOptions,
135 SerializeOptions, SinkDestination, SinkTarget, SpecialEq, UnifiedSinkArgs,
136 };
137 use std::sync::{Arc, Mutex};
138
139 let file_format = match format {
141 ExportFormat::Csv | ExportFormat::Tsv | ExportFormat::Psv => {
142 FileWriteFormat::Csv(CsvWriterOptions {
143 include_header: options.csv_include_header,
144 serialize_options: Arc::new(SerializeOptions {
145 separator: options.delimiter(format),
146 ..SerializeOptions::default()
147 }),
148 ..CsvWriterOptions::default()
149 })
150 }
151 ExportFormat::Parquet => FileWriteFormat::Parquet(Arc::new(ParquetWriteOptions {
154 compat_level: Some(CompatLevel::newest()),
155 ..ParquetWriteOptions::default()
156 })),
157 other => unreachable!("{other:?} does not stream"),
158 };
159 let target = SinkTarget::Dyn(SpecialEq::new(Arc::new(Mutex::new(Some(writable)))));
160 lf.sink(
161 SinkDestination::File { target },
162 file_format,
163 UnifiedSinkArgs::default(),
164 )?
165 .collect_with_engine(Engine::Streaming)?;
166 Ok(())
167}
168
169struct Counted<W, F> {
172 inner: W,
173 bytes: u64,
174 reported: Instant,
175 report: F,
176}
177
178impl<W: Write, F: FnMut(u64)> Counted<W, F> {
179 fn new(inner: W, report: F) -> Self {
180 Self {
181 inner,
182 bytes: 0,
183 reported: Instant::now(),
184 report,
185 }
186 }
187}
188
189impl<W: Write, F: FnMut(u64)> Write for Counted<W, F> {
190 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
191 let n = self.inner.write(buf)?;
192 self.bytes += n as u64;
193 if self.reported.elapsed() >= PROGRESS_EVERY {
194 self.reported = Instant::now();
195 (self.report)(self.bytes);
196 }
197 Ok(n)
198 }
199
200 fn flush(&mut self) -> io::Result<()> {
201 self.inner.flush()
202 }
203}
204
205#[cfg(feature = "streaming")]
209impl<F: FnMut(u64) + Send + 'static> Counted<std::fs::File, F> {
210 fn into_writable(self) -> polars::io::utils::file::Writable {
211 polars::io::utils::file::Writable::Dyn(Box::new(self))
212 }
213}
214
215#[cfg(feature = "streaming")]
216impl<F: FnMut(u64)> polars::io::utils::file::WritableTrait for Counted<std::fs::File, F> {
217 fn close(&mut self) -> io::Result<()> {
218 self.inner.flush()
219 }
220
221 fn sync_all(&self) -> io::Result<()> {
222 self.inner.sync_all()
223 }
224
225 fn sync_data(&self) -> io::Result<()> {
226 self.inner.sync_data()
227 }
228}
229
230pub fn encode<W: Write>(
233 df: &mut DataFrame,
234 format: ExportFormat,
235 options: &ExportOptions,
236 sink: W,
237) -> Result<()> {
238 let mut sink = match options.compression(format) {
240 None => {
241 let mut buffered = BufWriter::new(sink);
242 serialize(df, format, options, &mut buffered)?;
243 buffered
244 .into_inner()
245 .map_err(io::IntoInnerError::into_error)?
246 }
247 Some(compression) => {
248 let mut buffered = BufWriter::new(Encoder::new(compression, sink)?);
249 serialize(df, format, options, &mut buffered)?;
250 let encoder = buffered
251 .into_inner()
252 .map_err(io::IntoInnerError::into_error)?;
253 encoder.finish()?
254 }
255 };
256 sink.flush()?;
257 Ok(())
258}
259
260fn serialize(
261 df: &mut DataFrame,
262 format: ExportFormat,
263 options: &ExportOptions,
264 out: &mut impl Write,
265) -> Result<()> {
266 match format {
267 ExportFormat::Csv | ExportFormat::Tsv | ExportFormat::Psv => CsvWriter::new(out)
268 .with_separator(options.delimiter(format))
269 .include_header(options.csv_include_header)
270 .finish(df)?,
271 ExportFormat::Parquet => {
272 ParquetWriter::new(out).finish(df)?;
273 }
274 ExportFormat::Json => JsonWriter::new(out)
275 .with_json_format(JsonFormat::Json)
276 .finish(df)?,
277 ExportFormat::Ndjson => JsonWriter::new(out)
278 .with_json_format(JsonFormat::JsonLines)
279 .finish(df)?,
280 ExportFormat::Ipc => IpcWriter::new(out).finish(df)?,
281 ExportFormat::Avro => crate::export::avro_types::write(df, out)?,
282 }
283 Ok(())
284}
285
286enum Encoder<W: Write> {
290 Gzip(flate2::write::GzEncoder<W>),
291 Zstd(zstd::Encoder<'static, W>),
292 Bzip2(bzip2::write::BzEncoder<W>),
293 Xz(xz2::write::XzEncoder<W>),
294}
295
296impl<W: Write> Encoder<W> {
297 fn new(compression: CompressionFormat, out: W) -> io::Result<Self> {
298 Ok(match compression {
299 CompressionFormat::Gzip => Self::Gzip(flate2::write::GzEncoder::new(
300 out,
301 flate2::Compression::default(),
302 )),
303 CompressionFormat::Zstd => Self::Zstd(zstd::Encoder::new(out, 0)?),
304 CompressionFormat::Bzip2 => Self::Bzip2(bzip2::write::BzEncoder::new(
305 out,
306 bzip2::Compression::default(),
307 )),
308 CompressionFormat::Xz => Self::Xz(xz2::write::XzEncoder::new(out, 6)),
309 })
310 }
311
312 fn finish(self) -> io::Result<W> {
313 match self {
314 Self::Gzip(e) => e.finish(),
315 Self::Zstd(e) => e.finish(),
316 Self::Bzip2(e) => e.finish(),
317 Self::Xz(e) => e.finish(),
318 }
319 }
320}
321
322impl<W: Write> Write for Encoder<W> {
323 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
324 match self {
325 Self::Gzip(e) => e.write(buf),
326 Self::Zstd(e) => e.write(buf),
327 Self::Bzip2(e) => e.write(buf),
328 Self::Xz(e) => e.write(buf),
329 }
330 }
331
332 fn flush(&mut self) -> io::Result<()> {
333 match self {
334 Self::Gzip(e) => e.flush(),
335 Self::Zstd(e) => e.flush(),
336 Self::Bzip2(e) => e.flush(),
337 Self::Xz(e) => e.flush(),
338 }
339 }
340}
341
342#[cfg(test)]
343mod tests;