use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use parquet::file::reader::{FileReader, SerializedFileReader};
use parquet::record::{Row, RowAccessor};
use crate::common::error::AicError;
const LFS_POINTER_PREFIX: &[u8] = b"version https://git-lfs";
pub struct PerfReader {
file_reader: SerializedFileReader<File>,
path: PathBuf,
column_names: Vec<String>,
}
impl PerfReader {
pub fn open(path: &Path) -> Result<Self, AicError> {
let mut file = File::open(path).map_err(|source| AicError::Io {
path: path.to_path_buf(),
source,
})?;
let mut head = [0u8; LFS_POINTER_PREFIX.len()];
let read = file.read(&mut head).map_err(|source| AicError::Io {
path: path.to_path_buf(),
source,
})?;
if read >= LFS_POINTER_PREFIX.len() && &head == LFS_POINTER_PREFIX {
return Err(AicError::PerfDatabase(format!(
"perf file is an unresolved git-lfs pointer: {}; run `git lfs pull`",
path.display()
)));
}
let file = File::open(path).map_err(|source| AicError::Io {
path: path.to_path_buf(),
source,
})?;
let file_reader = SerializedFileReader::new(file).map_err(|source| AicError::Parquet {
path: path.to_path_buf(),
source,
})?;
let column_names = file_reader
.metadata()
.file_metadata()
.schema_descr()
.columns()
.iter()
.map(|c| c.name().to_string())
.collect();
Ok(Self {
file_reader,
path: path.to_path_buf(),
column_names,
})
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn col(&self, name: &str) -> Result<usize, AicError> {
self.column_names
.iter()
.position(|n| n == name)
.ok_or_else(|| {
AicError::PerfDatabase(format!(
"parquet column {name:?} missing at {} (have: [{}])",
self.path.display(),
self.column_names.join(", ")
))
})
}
pub fn col_optional(&self, name: &str) -> Option<usize> {
self.column_names.iter().position(|n| n == name)
}
pub fn rows(&self) -> Result<impl Iterator<Item = Result<PerfRow, AicError>> + '_, AicError> {
let row_iter = self
.file_reader
.get_row_iter(None)
.map_err(|source| AicError::Parquet {
path: self.path.clone(),
source,
})?;
let path = self.path.clone();
Ok(row_iter.map(move |r| {
r.map(|row| PerfRow {
row,
path: path.clone(),
})
.map_err(|source| AicError::Parquet {
path: path.clone(),
source,
})
}))
}
}
pub struct PerfRow {
row: Row,
path: PathBuf,
}
impl PerfRow {
pub fn str(&self, col: usize) -> Result<&str, AicError> {
self.row
.get_string(col)
.map(String::as_str)
.map_err(|source| AicError::Parquet {
path: self.path.clone(),
source,
})
}
pub fn str_owned(&self, col: usize) -> Result<String, AicError> {
self.str(col).map(|s| s.to_string())
}
pub fn u32(&self, col: usize) -> Result<u32, AicError> {
let v = self.row.get_long(col).map_err(|source| AicError::Parquet {
path: self.path.clone(),
source,
})?;
u32::try_from(v).map_err(|_| {
AicError::PerfDatabase(format!(
"parquet column[{col}] = {v} does not fit in u32 at {}",
self.path.display()
))
})
}
pub fn u32_optional(&self, col: Option<usize>) -> Result<Option<u32>, AicError> {
let Some(idx) = col else {
return Ok(None);
};
match self.row.get_long(idx) {
Ok(v) => u32::try_from(v).map(Some).map_err(|_| {
AicError::PerfDatabase(format!(
"parquet column[{idx}] = {v} does not fit in u32 at {}",
self.path.display()
))
}),
Err(parquet::errors::ParquetError::General(_)) => Ok(None),
Err(source) => Err(AicError::Parquet {
path: self.path.clone(),
source,
}),
}
}
pub fn i64_optional(&self, col: Option<usize>) -> Result<Option<i64>, AicError> {
let Some(idx) = col else {
return Ok(None);
};
match self.row.get_long(idx) {
Ok(v) => Ok(Some(v)),
Err(parquet::errors::ParquetError::General(_)) => Ok(None),
Err(source) => Err(AicError::Parquet {
path: self.path.clone(),
source,
}),
}
}
pub fn u64(&self, col: usize) -> Result<u64, AicError> {
let v = self.row.get_long(col).map_err(|source| AicError::Parquet {
path: self.path.clone(),
source,
})?;
u64::try_from(v).map_err(|_| {
AicError::PerfDatabase(format!(
"parquet column[{col}] = {v} is negative; expected u64 at {}",
self.path.display()
))
})
}
pub fn f64(&self, col: usize) -> Result<f64, AicError> {
self.row
.get_double(col)
.map_err(|source| AicError::Parquet {
path: self.path.clone(),
source,
})
}
pub fn bool(&self, col: usize) -> Result<bool, AicError> {
if let Ok(v) = self.row.get_bool(col) {
return Ok(v);
}
if let Ok(v) = self.row.get_long(col) {
return Ok(v == 1);
}
let s = self.str(col)?;
Ok(matches!(
s.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "y"
))
}
pub fn bool_strict(&self, col: usize) -> Result<bool, AicError> {
self.row.get_bool(col).map_err(|source| AicError::Parquet {
path: self.path.clone(),
source,
})
}
pub fn f64_optional(&self, col: Option<usize>) -> Result<Option<f64>, AicError> {
let Some(idx) = col else {
return Ok(None);
};
match self.row.get_double(idx) {
Ok(v) => Ok(Some(v)),
Err(parquet::errors::ParquetError::General(_)) => Ok(None),
Err(source) => Err(AicError::Parquet {
path: self.path.clone(),
source,
}),
}
}
pub fn str_optional(&self, col: Option<usize>) -> Result<Option<&str>, AicError> {
let Some(idx) = col else {
return Ok(None);
};
match self.row.get_string(idx) {
Ok(s) => Ok(Some(s.as_str())),
Err(parquet::errors::ParquetError::General(_)) => Ok(None),
Err(source) => Err(AicError::Parquet {
path: self.path.clone(),
source,
}),
}
}
}