use crate::executor::expression::{ExpressionError, ExpressionErrorType};
use linkrs_core::Value;
pub trait TableFunction: Send + Sync + std::fmt::Debug {
fn name(&self) -> &str;
fn execute(&self, args: &[Value]) -> Result<Vec<Vec<Value>>, ExpressionError>;
fn description(&self) -> &str;
}
#[derive(Debug, Clone)]
pub enum BuiltinTableFunction {
ReadCsv,
}
impl TableFunction for BuiltinTableFunction {
fn name(&self) -> &str {
match self {
BuiltinTableFunction::ReadCsv => "read_csv",
}
}
fn execute(&self, args: &[Value]) -> Result<Vec<Vec<Value>>, ExpressionError> {
match self {
BuiltinTableFunction::ReadCsv => execute_read_csv(args),
}
}
fn description(&self) -> &str {
match self {
BuiltinTableFunction::ReadCsv => "Read a CSV file and return its rows",
}
}
}
fn execute_read_csv(args: &[Value]) -> Result<Vec<Vec<Value>>, ExpressionError> {
if args.is_empty() {
return Err(ExpressionError::new(
ExpressionErrorType::InvalidArgumentCount,
"read_csv expects at least 1 argument (path)".to_string(),
));
}
let path = match &args[0] {
Value::String(s) => s.clone(),
other => {
return Err(ExpressionError::new(
ExpressionErrorType::TypeError,
format!("read_csv path must be a string, got {:?}", other),
))
}
};
let delimiter = if args.len() > 1 {
match &args[1] {
Value::String(s) => s.as_bytes().first().copied().unwrap_or(b','),
_ => b',',
}
} else {
b','
};
let file = std::fs::File::open(&path).map_err(|e| {
ExpressionError::new(
ExpressionErrorType::FunctionExecutionError,
format!("read_csv failed to open '{path}': {e}"),
)
})?;
let mut reader = csv::ReaderBuilder::new()
.has_headers(true)
.delimiter(delimiter)
.trim(csv::Trim::All)
.flexible(true)
.from_reader(std::io::BufReader::new(file));
let mut rows: Vec<Vec<Value>> = Vec::new();
for result in reader.records() {
let record = result.map_err(|e| {
ExpressionError::new(
ExpressionErrorType::FunctionExecutionError,
format!("read_csv row error: {e}"),
)
})?;
let row: Vec<Value> = record.iter().map(|s| Value::String(s.into())).collect();
rows.push(row);
}
Ok(rows)
}