use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use datacraft_core::port::{
DatabaseFormat, DriverId, PortError, RowRange, SchemaChange, StoragePort, Support,
};
use datacraft_core::schema::{FieldDef, FieldKind, FieldValue, TableDef};
use datacraft_csv::CsvPort;
fn scratch_path(stem: &str) -> PathBuf {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!("datacraft-csv-{}-{n}-{stem}", std::process::id()))
}
struct Scratch(PathBuf);
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
let _ = std::fs::remove_file(&self.0);
}
}
fn csv_database() -> Scratch {
let dir = scratch_path("data");
std::fs::create_dir_all(&dir).expect("create the directory");
std::fs::write(
dir.join("people.csv"),
"name,city\nada,london\ngrace,\"new york\"\n",
)
.expect("write the csv file");
Scratch(dir)
}
#[test]
fn the_csv_driver_reports_its_honest_capabilities() {
let scratch = csv_database();
let mut port = CsvPort::new();
port.open(&scratch.0, None).expect("open the directory");
assert_eq!(port.driver_id(), DriverId::new("csv"));
let caps = port.capabilities();
assert!(caps.read_tables);
assert!(caps.write_rows);
assert!(caps.create_table);
assert!(caps.drop_table);
assert!(caps.create_database);
assert!(!caps.alter_table, "CSV has no alter path");
assert!(!caps.rename_table, "CSV has no table rename path");
assert!(caps.atomic_batch, "one rewrite per call is atomic");
assert!(!caps.passwords);
assert_eq!(caps.autonumber, Support::No);
assert_eq!(caps.relationships, Support::No);
assert_eq!(caps.native_queries, Support::No);
assert_eq!(caps.macros, Support::No);
assert_eq!(caps.vba, Support::No);
assert_eq!(caps.triggers, Support::No);
assert_eq!(caps.stored_procedures, Support::No);
}
#[test]
fn csv_tables_come_from_the_directory_listing() {
let scratch = csv_database();
let mut port = CsvPort::new();
port.open(&scratch.0, None).expect("open the directory");
let tables = port.tables();
assert_eq!(tables.len(), 1, "{tables:?}");
assert_eq!(tables[0].name, "people");
assert_eq!(tables[0].fields.len(), 2);
assert!(matches!(tables[0].fields[0].kind, FieldKind::Text { .. }));
assert_eq!(tables[0].fields[0].name, "name");
assert_eq!(tables[0].fields[1].name, "city");
let rows = port
.read_rows("people", RowRange { start: 0, len: 10 })
.expect("read the rows");
assert_eq!(
rows,
vec![
vec![
FieldValue::Text("ada".to_owned()),
FieldValue::Text("london".to_owned()),
],
vec![
FieldValue::Text("grace".to_owned()),
FieldValue::Text("new york".to_owned()),
],
]
);
}
#[test]
fn csv_writes_survive_a_reopen() {
let scratch = csv_database();
let mut port = CsvPort::new();
port.open(&scratch.0, None).expect("open the directory");
port.insert_row(
"people",
&[
FieldValue::Text("alan".to_owned()),
FieldValue::Text("wilmslow".to_owned()),
],
)
.expect("append the row");
port.update_row(
"people",
0,
&[
FieldValue::Text("ada l".to_owned()),
FieldValue::Text("london".to_owned()),
],
)
.expect("update the first row");
port.delete_row("people", 1).expect("delete the second row");
let mut reopened = CsvPort::new();
reopened.open(&scratch.0, None).expect("reopen");
let rows = reopened
.read_rows("people", RowRange { start: 0, len: 10 })
.expect("read after reopen");
assert_eq!(rows.len(), 2);
assert_eq!(rows[0][0], FieldValue::Text("ada l".to_owned()));
assert_eq!(rows[1][0], FieldValue::Text("alan".to_owned()));
let error = reopened
.insert_row("people", &[FieldValue::Text("one".to_owned())])
.expect_err("arity is checked against the header");
assert!(matches!(error, PortError::WriteFailed { .. }), "{error:?}");
}
#[test]
fn csv_create_database_and_create_table_behave() {
let scratch = Scratch(scratch_path("fresh"));
let mut port = CsvPort::new();
port.create_database(&scratch.0, DatabaseFormat::Csv)
.expect("create the directory database");
assert!(scratch.0.is_dir());
let error = port
.create_database(&scratch.0, DatabaseFormat::Csv)
.expect_err("creating twice is refused");
assert!(matches!(error, PortError::WriteFailed { .. }), "{error:?}");
let error = port
.create_database(&scratch.0, DatabaseFormat::Jet4)
.expect_err("foreign formats are refused");
assert!(matches!(error, PortError::Unsupported { .. }), "{error:?}");
port.open(&scratch.0, None)
.expect("open the empty directory");
port.create_table(&TableDef::new(
"places",
vec![
FieldDef::new("name", FieldKind::Text { max_len: 255 }),
FieldDef::new("country", FieldKind::Text { max_len: 255 }),
],
))
.expect("create the header-only file");
assert!(scratch.0.join("places.csv").exists());
assert_eq!(port.tables()[0].name, "places");
}
#[test]
fn csv_capability_gaps_answer_precisely() {
let scratch = csv_database();
let mut port = CsvPort::new();
port.open(&scratch.0, None).expect("open the directory");
let error = port
.alter_table(
"people",
SchemaChange::RenameField {
field: "name".to_owned(),
new_name: "who".to_owned(),
},
)
.expect_err("alter_table is unsupported on CSV");
assert!(matches!(error, PortError::Unsupported { .. }), "{error:?}");
let error = port
.rename_table("people", "folks")
.expect_err("rename_table is unsupported on CSV");
assert!(matches!(error, PortError::Unsupported { .. }), "{error:?}");
assert!(port.relationships().expect("none").is_empty());
let file = scratch_path("plain.txt");
std::fs::write(&file, "not a database").expect("write the file");
let mut stranger = CsvPort::new();
let error = stranger
.open(&file, None)
.expect_err("a plain file is no CSV directory");
assert!(
matches!(
error,
PortError::OpenFailed { .. } | PortError::Unsupported { .. }
),
"{error:?}"
);
assert!(error.to_string().contains(&file.display().to_string()));
let _ = std::fs::remove_file(&file);
}
#[test]
fn csv_rejects_hostile_names_and_ragged_rows() {
let scratch = csv_database();
let mut port = CsvPort::new();
port.open(&scratch.0, None).expect("open the directory");
let error = port
.create_table(&TableDef::new(
"../escape",
vec![FieldDef::new("a", FieldKind::Text { max_len: 255 })],
))
.expect_err("path separators are rejected");
assert!(matches!(error, PortError::WriteFailed { .. }), "{error:?}");
std::fs::write(scratch.0.join("ragged.csv"), "a,b\n1\n").expect("write ragged");
let mut stranger = CsvPort::new();
let error = stranger
.open(&scratch.0, None)
.expect_err("a ragged row fails the open");
assert!(error.to_string().contains("ragged"), "{error}");
let _ = std::fs::remove_file(scratch.0.join("ragged.csv"));
}
#[test]
fn create_table_validates_the_definition_before_writing() {
let scratch = csv_database();
let mut port = CsvPort::new();
port.open(&scratch.0, None).expect("open the directory");
let invalid = TableDef::new(
"dupes",
vec![
FieldDef::new("a", FieldKind::Text { max_len: 10 }),
FieldDef::new("A", FieldKind::Text { max_len: 10 }),
],
);
let err = port
.create_table(&invalid)
.expect_err("duplicate field names must be refused");
assert!(
err.to_string().to_lowercase().contains("duplicate"),
"{err}"
);
assert!(
!scratch.0.join("dupes.csv").exists(),
"no file may be written before validation"
);
let mut reopened = CsvPort::new();
reopened
.open(&scratch.0, None)
.expect("directory still opens");
assert_eq!(reopened.tables().len(), 1);
}