| // Licensed to the Apache Software Foundation (ASF) under one | |
| // or more contributor license agreements. See the NOTICE file | |
| // distributed with this work for additional information | |
| // regarding copyright ownership. The ASF licenses this file | |
| // to you under the Apache License, Version 2.0 (the | |
| // "License"); you may not use this file except in compliance | |
| // with the License. You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, | |
| // software distributed under the License is distributed on an | |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| // KIND, either express or implied. See the License for the | |
| // specific language governing permissions and limitations | |
| // under the License. | |
| use std::path::{Path, PathBuf}; | |
| use datafusion::dataframe::DataFrameWriteOptions; | |
| use datafusion::error::{DataFusionError, Result}; | |
| use datafusion::prelude::{CsvReadOptions, SessionContext}; | |
| use tempfile::TempDir; | |
| use tokio::fs::create_dir_all; | |
| /// Temporary Parquet directory that is deleted when dropped. | |
| #[derive(Debug)] | |
| pub struct ParquetTemp { | |
| pub tmp_dir: TempDir, | |
| pub parquet_dir: PathBuf, | |
| } | |
| impl ParquetTemp { | |
| pub fn path(&self) -> &Path { | |
| &self.parquet_dir | |
| } | |
| pub fn path_str(&self) -> Result<&str> { | |
| self.parquet_dir.to_str().ok_or_else(|| { | |
| DataFusionError::Execution(format!( | |
| "Parquet directory path is not valid UTF-8: {}", | |
| self.parquet_dir.display() | |
| )) | |
| }) | |
| } | |
| pub fn file_uri(&self) -> Result<String> { | |
| Ok(format!("file://{}", self.path_str()?)) | |
| } | |
| } | |
| /// Helper for examples: load a CSV file and materialize it as Parquet | |
| /// in a temporary directory. | |
| /// | |
| /// # Example | |
| /// ``` | |
| /// use std::path::PathBuf; | |
| /// use datafusion::prelude::*; | |
| /// use datafusion_examples::utils::write_csv_to_parquet; | |
| /// # use datafusion::assert_batches_eq; | |
| /// # use datafusion::error::Result; | |
| /// # #[tokio::main] | |
| /// # async fn main() -> Result<()> { | |
| /// let ctx = SessionContext::new(); | |
| /// let csv_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) | |
| /// .join("data") | |
| /// .join("csv") | |
| /// .join("cars.csv"); | |
| /// let parquet_dir = write_csv_to_parquet(&ctx, &csv_path).await?; | |
| /// let df = ctx.read_parquet(parquet_dir.path_str()?, ParquetReadOptions::default()).await?; | |
| /// let rows = df | |
| /// .sort(vec![col("speed").sort(true, true)])? | |
| /// .limit(0, Some(5))?; | |
| /// assert_batches_eq!( | |
| /// &[ | |
| /// "+-------+-------+---------------------+", | |
| /// "| car | speed | time |", | |
| /// "+-------+-------+---------------------+", | |
| /// "| red | 0.0 | 1996-04-12T12:05:15 |", | |
| /// "| red | 1.0 | 1996-04-12T12:05:14 |", | |
| /// "| green | 2.0 | 1996-04-12T12:05:14 |", | |
| /// "| red | 3.0 | 1996-04-12T12:05:13 |", | |
| /// "| red | 7.0 | 1996-04-12T12:05:10 |", | |
| /// "+-------+-------+---------------------+", | |
| /// ], | |
| /// &rows.collect().await? | |
| /// ); | |
| /// # Ok(()) | |
| /// # } | |
| /// ``` | |
| pub async fn write_csv_to_parquet( | |
| ctx: &SessionContext, | |
| csv_path: &Path, | |
| ) -> Result<ParquetTemp> { | |
| if !csv_path.is_file() { | |
| return Err(DataFusionError::Execution(format!( | |
| "CSV file does not exist: {}", | |
| csv_path.display() | |
| ))); | |
| } | |
| let csv_path = csv_path.to_str().ok_or_else(|| { | |
| DataFusionError::Execution("CSV path is not valid UTF-8".to_string()) | |
| })?; | |
| let csv_df = ctx.read_csv(csv_path, CsvReadOptions::default()).await?; | |
| let tmp_dir = TempDir::new()?; | |
| let parquet_dir = tmp_dir.path().join("parquet_source"); | |
| create_dir_all(&parquet_dir).await?; | |
| let path = parquet_dir.to_str().ok_or_else(|| { | |
| DataFusionError::Execution("Failed processing tmp directory path".to_string()) | |
| })?; | |
| csv_df | |
| .write_parquet(path, DataFrameWriteOptions::default(), None) | |
| .await?; | |
| Ok(ParquetTemp { | |
| tmp_dir, | |
| parquet_dir, | |
| }) | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| use std::path::PathBuf; | |
| use datafusion::assert_batches_eq; | |
| use datafusion::prelude::*; | |
| #[tokio::test] | |
| async fn test_write_csv_to_parquet_with_cars_data() -> Result<()> { | |
| let ctx = SessionContext::new(); | |
| let csv_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) | |
| .join("data") | |
| .join("csv") | |
| .join("cars.csv"); | |
| let parquet_dir = write_csv_to_parquet(&ctx, &csv_path).await?; | |
| let df = ctx | |
| .read_parquet(parquet_dir.path_str()?, ParquetReadOptions::default()) | |
| .await?; | |
| let rows = df.sort(vec![col("speed").sort(true, true)])?; | |
| assert_batches_eq!( | |
| &[ | |
| "+-------+-------+---------------------+", | |
| "| car | speed | time |", | |
| "+-------+-------+---------------------+", | |
| "| red | 0.0 | 1996-04-12T12:05:15 |", | |
| "| red | 1.0 | 1996-04-12T12:05:14 |", | |
| "| green | 2.0 | 1996-04-12T12:05:14 |", | |
| "| red | 3.0 | 1996-04-12T12:05:13 |", | |
| "| red | 7.0 | 1996-04-12T12:05:10 |", | |
| "| red | 7.1 | 1996-04-12T12:05:11 |", | |
| "| red | 7.2 | 1996-04-12T12:05:12 |", | |
| "| green | 8.0 | 1996-04-12T12:05:13 |", | |
| "| green | 10.0 | 1996-04-12T12:05:03 |", | |
| "| green | 10.3 | 1996-04-12T12:05:04 |", | |
| "| green | 10.4 | 1996-04-12T12:05:05 |", | |
| "| green | 10.5 | 1996-04-12T12:05:06 |", | |
| "| green | 11.0 | 1996-04-12T12:05:07 |", | |
| "| green | 12.0 | 1996-04-12T12:05:08 |", | |
| "| green | 14.0 | 1996-04-12T12:05:09 |", | |
| "| green | 15.0 | 1996-04-12T12:05:10 |", | |
| "| green | 15.1 | 1996-04-12T12:05:11 |", | |
| "| green | 15.2 | 1996-04-12T12:05:12 |", | |
| "| red | 17.0 | 1996-04-12T12:05:09 |", | |
| "| red | 18.0 | 1996-04-12T12:05:08 |", | |
| "| red | 19.0 | 1996-04-12T12:05:07 |", | |
| "| red | 20.0 | 1996-04-12T12:05:03 |", | |
| "| red | 20.3 | 1996-04-12T12:05:04 |", | |
| "| red | 21.4 | 1996-04-12T12:05:05 |", | |
| "| red | 21.5 | 1996-04-12T12:05:06 |", | |
| "+-------+-------+---------------------+", | |
| ], | |
| &rows.collect().await? | |
| ); | |
| Ok(()) | |
| } | |
| #[tokio::test] | |
| async fn test_write_csv_to_parquet_with_regex_data() -> Result<()> { | |
| let ctx = SessionContext::new(); | |
| let csv_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) | |
| .join("data") | |
| .join("csv") | |
| .join("regex.csv"); | |
| let parquet_dir = write_csv_to_parquet(&ctx, &csv_path).await?; | |
| let df = ctx | |
| .read_parquet(parquet_dir.path_str()?, ParquetReadOptions::default()) | |
| .await?; | |
| let rows = df.sort(vec![col("values").sort(true, true)])?; | |
| assert_batches_eq!( | |
| &[ | |
| "+------------+--------------------------------------+-------------+-------+", | |
| "| values | patterns | replacement | flags |", | |
| "+------------+--------------------------------------+-------------+-------+", | |
| "| 4000 | \\b4([1-9]\\d\\d|\\d[1-9]\\d|\\d\\d[1-9])\\b | xyz | |", | |
| "| 4010 | \\b4([1-9]\\d\\d|\\d[1-9]\\d|\\d\\d[1-9])\\b | xyz | |", | |
| "| ABC | ^(A).* | B | i |", | |
| "| AbC | (B|D) | e | |", | |
| "| Düsseldorf | [\\p{Letter}-]+ | München | |", | |
| "| Köln | [a-zA-Z]ö[a-zA-Z]{2} | Koln | |", | |
| "| aBC | ^(b|c) | d | |", | |
| "| aBc | (b|d) | e | i |", | |
| "| abc | ^(a) | bb\\1bb | i |", | |
| "| Москва | [\\p{L}-]+ | Moscow | |", | |
| "| اليوم | ^\\p{Arabic}+$ | Today | |", | |
| "+------------+--------------------------------------+-------------+-------+", | |
| ], | |
| &rows.collect().await? | |
| ); | |
| Ok(()) | |
| } | |
| #[tokio::test] | |
| async fn test_write_csv_to_parquet_error() { | |
| let ctx = SessionContext::new(); | |
| let csv_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) | |
| .join("data") | |
| .join("csv") | |
| .join("file-does-not-exist.csv"); | |
| let err = write_csv_to_parquet(&ctx, &csv_path).await.unwrap_err(); | |
| match err { | |
| DataFusionError::Execution(msg) => { | |
| assert!( | |
| msg.contains("CSV file does not exist"), | |
| "unexpected error message: {msg}" | |
| ); | |
| } | |
| other => panic!("unexpected error variant: {other:?}"), | |
| } | |
| } | |
| } |