use std::borrow::Cow;
use std::fs::File;
use std::path::Path;
use memmap2::Mmap;
use crate::capacity::{
estimate_row_count, generate_alpha_columns, is_comment_line, is_likely_data_row,
DEFAULT_ROW_CAPACITY,
};
use crate::error::{SqawkError, SqawkResult};
use crate::table::{Row, Value};
#[derive(Debug)]
pub struct MmapStorage {
#[allow(dead_code)]
mmap: Mmap,
columns: Vec<String>,
rows: Vec<Row>,
}
#[allow(dead_code)]
impl MmapStorage {
pub fn open<P: AsRef<Path>>(path: P, delimiter: u8) -> SqawkResult<Self> {
Self::open_with_columns(path, delimiter, None)
}
pub fn open_with_columns<P: AsRef<Path>>(
path: P,
delimiter: u8,
predefined_columns: Option<Vec<String>>,
) -> SqawkResult<Self> {
let file = File::open(path.as_ref()).map_err(SqawkError::IoError)?;
let mmap = unsafe { Mmap::map(&file).map_err(SqawkError::IoError)? };
#[cfg(unix)]
{
let ptr = mmap.as_ptr() as *mut libc::c_void;
let len = mmap.len();
unsafe {
libc::madvise(ptr, len, libc::MADV_SEQUENTIAL);
libc::madvise(ptr, len, libc::MADV_WILLNEED);
}
}
let (columns, rows) = Self::parse_csv(&mmap, delimiter, predefined_columns)?;
Ok(Self {
mmap,
columns,
rows,
})
}
fn parse_csv(
data: &Mmap,
delimiter: u8,
predefined_columns: Option<Vec<String>>,
) -> SqawkResult<(Vec<String>, Vec<Row>)> {
if data.is_empty() {
return Ok((Vec::new(), Vec::new()));
}
let estimated_lines = estimate_row_count(data.len());
let mut lines: Vec<&[u8]> = Vec::with_capacity(estimated_lines);
let mut start = 0;
for (i, &byte) in data.iter().enumerate() {
if byte == b'\n' {
let end = if i > 0 && data[i - 1] == b'\r' {
i - 1
} else {
i
};
if end > start {
lines.push(&data[start..end]);
}
start = i + 1;
}
}
if start < data.len() {
let end = if data[data.len() - 1] == b'\r' {
data.len() - 1
} else {
data.len()
};
if end > start {
lines.push(&data[start..end]);
}
}
lines.retain(|line| !is_comment_line(line));
if lines.is_empty() {
return Ok((Vec::new(), Vec::new()));
}
let (columns, data_start) = if let Some(cols) = predefined_columns {
(cols, 0)
} else {
let header_line = lines[0];
let fields: Vec<String> = Self::split_fields(header_line, delimiter)
.iter()
.map(|field| String::from_utf8_lossy(field).into_owned())
.collect();
if is_likely_data_row(&fields) {
(generate_alpha_columns(fields.len()), 0)
} else {
(fields, 1)
}
};
let mut rows = Vec::with_capacity(lines.len() - data_start);
for line in lines.iter().skip(data_start) {
let fields = Self::split_fields(line, delimiter);
let mut row = Vec::with_capacity(columns.len());
for field in fields {
let value = unsafe { Self::parse_field_borrowed(field) };
row.push(value);
}
while row.len() < columns.len() {
row.push(Value::Null);
}
rows.push(row);
}
Ok((columns, rows))
}
fn split_fields(line: &[u8], delimiter: u8) -> Vec<&[u8]> {
let mut fields = Vec::with_capacity(DEFAULT_ROW_CAPACITY);
let mut start = 0;
let mut in_quotes = false;
for (i, &byte) in line.iter().enumerate() {
if byte == b'"' {
in_quotes = !in_quotes;
} else if byte == delimiter && !in_quotes {
fields.push(&line[start..i]);
start = i + 1;
}
}
fields.push(&line[start..]);
fields
}
unsafe fn parse_field_borrowed(field: &[u8]) -> Value {
let s = match std::str::from_utf8(field) {
Ok(s) => s,
Err(_) => {
return Value::String(Cow::Owned(String::from_utf8_lossy(field).into_owned()));
}
};
let trimmed = s.trim();
let trimmed = if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
&trimmed[1..trimmed.len() - 1]
} else {
trimmed
};
if let Ok(i) = trimmed.parse::<i64>() {
return Value::Integer(i);
}
if let Ok(f) = trimmed.parse::<f64>() {
return Value::Float(f);
}
if trimmed.is_empty() {
return Value::Null;
}
if trimmed.eq_ignore_ascii_case("true") || trimmed.eq_ignore_ascii_case("yes") {
return Value::Boolean(true);
}
if trimmed.eq_ignore_ascii_case("false") || trimmed.eq_ignore_ascii_case("no") {
return Value::Boolean(false);
}
let static_str: &'static str = std::mem::transmute(trimmed);
Value::String(Cow::Borrowed(static_str))
}
pub fn columns(&self) -> &[String] {
&self.columns
}
pub fn row_count(&self) -> usize {
self.rows.len()
}
pub fn rows(&self) -> &[Row] {
&self.rows
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_mmap_storage_basic() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "name,age,city").unwrap();
writeln!(file, "Alice,30,NYC").unwrap();
writeln!(file, "Bob,25,LA").unwrap();
let storage = MmapStorage::open(file.path(), b',').unwrap();
assert_eq!(storage.columns(), &["name", "age", "city"]);
assert_eq!(storage.row_count(), 2);
let rows = storage.rows();
assert_eq!(rows[0].len(), 3);
match &rows[0][0] {
Value::String(cow) => {
assert!(matches!(cow, Cow::Borrowed(_)));
assert_eq!(cow.as_ref(), "Alice");
}
_ => panic!("Expected string value"),
}
assert_eq!(rows[0][1], Value::Integer(30));
match &rows[0][2] {
Value::String(cow) => {
assert!(matches!(cow, Cow::Borrowed(_)));
assert_eq!(cow.as_ref(), "NYC");
}
_ => panic!("Expected string value"),
}
}
#[test]
fn test_mmap_storage_empty_file() {
let file = NamedTempFile::new().unwrap();
let storage = MmapStorage::open(file.path(), b',').unwrap();
assert!(storage.columns().is_empty());
assert_eq!(storage.row_count(), 0);
}
#[test]
fn test_mmap_storage_tsv() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "name\tvalue").unwrap();
writeln!(file, "test\t42").unwrap();
let storage = MmapStorage::open(file.path(), b'\t').unwrap();
assert_eq!(storage.columns(), &["name", "value"]);
assert_eq!(storage.row_count(), 1);
let rows = storage.rows();
assert_eq!(rows[0][1], Value::Integer(42));
}
}