use crate::data::dataframe::DataFrame;
use crate::data::doc::{Doc, Format};
use crate::data::io;
use serde_json::Value;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
#[derive(Clone, PartialEq, Eq)]
pub struct Source {
pub path: PathBuf,
pub container: Option<String>,
pub delimiter: Option<u8>,
pub format: Option<String>,
}
impl Source {
pub fn from_json(value: &Value) -> Result<Self, String> {
if let Some(path) = value.as_str() {
return Ok(Self {
path: PathBuf::from(path),
container: None,
delimiter: None,
format: None,
});
}
let obj = value.as_object().ok_or_else(|| {
"'source' must be an object with a 'path', or a path string".to_string()
})?;
let path = obj
.get("path")
.and_then(Value::as_str)
.ok_or_else(|| "'source' requires a 'path'".to_string())?;
let delimiter = match obj.get("delimiter").and_then(Value::as_str) {
Some(d) => {
let mut chars = d.chars();
match (chars.next(), chars.next()) {
(Some(c), None) if c.is_ascii() => Some(c as u8),
_ => {
return Err(format!(
"'delimiter' must be a single ASCII character, got {:?}",
d
))
}
}
}
None => None,
};
Ok(Self {
path: PathBuf::from(path),
container: obj
.get("container")
.and_then(Value::as_str)
.map(str::to_string),
delimiter,
format: obj
.get("format")
.and_then(Value::as_str)
.map(|s| s.to_lowercase()),
})
}
fn extension(&self) -> String {
self.format.clone().unwrap_or_else(|| {
self.path
.extension()
.and_then(|e| e.to_str())
.unwrap_or_default()
.to_lowercase()
})
}
}
const CACHE_ENTRIES: usize = 4;
pub struct Cached {
source: Source,
stamp: Stamp,
df: DataFrame,
}
type Stamp = Vec<(Option<SystemTime>, Option<u64>)>;
fn stamp_of(path: &Path) -> Stamp {
let one = |p: &Path| match std::fs::metadata(p) {
Ok(m) => (m.modified().ok(), Some(m.len())),
Err(_) => (None, None),
};
let name = path.to_string_lossy();
let mut out = vec![one(path)];
for extra in [
format!("{}-wal", name),
format!("{}.wal", name),
format!("{}-shm", name),
] {
out.push(one(Path::new(&extra)));
}
out
}
pub fn load(server: &mut super::Server, source: &Source) -> Result<DataFrame, String> {
let stamp = stamp_of(&source.path);
let readable = stamp[0].0.is_some();
if readable {
if let Some(i) = server
.cache
.iter()
.position(|c| c.source == *source && c.stamp == stamp)
{
let hit = server.cache.remove(i);
let df = hit.df.clone();
server.cache.insert(0, hit);
return Ok(df);
}
}
let df = load_once(source)?;
server.cache.retain(|c| c.source != *source);
server.cache.insert(
0,
Cached {
source: source.clone(),
stamp,
df: df.clone(),
},
);
server.cache.truncate(CACHE_ENTRIES);
Ok(df)
}
pub fn load_once(source: &Source) -> Result<DataFrame, String> {
if !source.path.exists() {
return Err(format!("No such file: {}", source.path.display()));
}
let ext = source.extension();
if let Some(container) = &source.container {
return load_container(&source.path, &ext, container);
}
if crate::data::io::db_write::is_db_name(&ext) {
let n = io::db_containers(&source.path)
.map(|c| c.len())
.unwrap_or(0);
return Err(format!(
"'{}' holds {} tables and views; pass 'container' to pick one. \
tuitab_inspect lists them.",
source.path.display(),
n
));
}
if let Some(declared) = source.format.as_deref() {
if Format::from_name(declared).is_none() {
return io::load_tabular(&source.path, source.delimiter, &ext).map_err(|e| {
format!("Could not read {} as {}: {}", source.path.display(), ext, e)
});
}
}
let forced = source.format.as_deref().and_then(Format::from_name);
io::load_file_as(&source.path, source.delimiter, forced)
.map(|(df, _)| df)
.map_err(|e| format!("Could not read {}: {}", source.path.display(), e))
}
pub fn load_db_table(
path: &Path,
container: &str,
) -> Result<(DataFrame, Option<io::db_write::TableSource>), String> {
match crate::data::io::db_write::kind_for_path(path) {
crate::data::io::db_write::DbKind::DuckDb => io::load_duckdb_table_full(path, container),
crate::data::io::db_write::DbKind::Sqlite => io::load_sqlite_table_full(path, container),
}
.map_err(|e| {
format!(
"Could not read '{}' from {}: {}",
container,
path.display(),
e
)
})
}
fn load_container(path: &Path, ext: &str, container: &str) -> Result<DataFrame, String> {
if crate::data::io::db_write::is_db_ext(path) {
return load_db_table(path, container).map(|(df, _)| df);
}
match ext {
"xlsx" | "xls" => io::load_excel_sheet_by_name(path, container).map_err(|e| {
format!(
"Could not read sheet '{}' from {}: {}",
container,
path.display(),
e
)
}),
other => Err(format!(
"'.{}' files hold a single table — drop 'container'",
other
)),
}
}
pub fn containers(path: &Path, ext: &str) -> Option<Vec<io::ContainerInfo>> {
if crate::data::io::db_write::is_db_ext(path) {
return io::db_containers(path).ok().filter(|c| !c.is_empty());
}
match ext {
"xlsx" | "xls" => io::excel_sheet_sizes(path).ok().map(|sheets| {
sheets
.into_iter()
.map(|(name, rows, columns)| io::ContainerInfo {
name,
view: false,
rows: Some(rows as i64),
columns,
sql: None,
})
.collect()
}),
_ => None,
}
}
pub fn extension_of(source: &Source) -> String {
source.extension()
}
pub fn load_doc(source: &Source) -> Result<Doc, String> {
let ext = source.extension();
let format = Format::from_name(&ext).ok_or_else(|| {
format!(
"jq needs a JSON, JSONL, YAML or TOML source; '{}' is not one. \
Use tuitab_query for tabular data.",
if ext.is_empty() {
"(no extension)"
} else {
&ext
}
)
})?;
Doc::load(&source.path, format)
.map_err(|e| format!("Could not read {}: {}", source.path.display(), e))
}