1use std::path::{Path, PathBuf};
27
28use datacraft_core::import::{infer_field_kinds, quote_mask};
29
30use datacraft_core::port::{
31 DatabaseFormat, DriverCapabilities, DriverId, OpenedDatabase, PortError, Relationship,
32 RowRange, StoragePort, Support, window_slice,
33};
34use datacraft_core::schema::{FieldDef, FieldValue, TableDef};
35
36#[derive(Debug, Default)]
38pub struct CsvPort {
39 dir: Option<PathBuf>,
40 tables: Vec<TableDef>,
41}
42
43impl CsvPort {
44 pub fn new() -> Self {
46 Self::default()
47 }
48}
49
50impl StoragePort for CsvPort {
51 fn driver_id(&self) -> DriverId {
52 DriverId::new("csv")
53 }
54
55 fn source_format(&self) -> Option<DatabaseFormat> {
58 Some(DatabaseFormat::Csv)
59 }
60
61 fn capabilities(&self) -> DriverCapabilities {
66 DriverCapabilities {
67 read_tables: true,
68 write_rows: true,
69 create_table: true,
70 drop_table: true,
71 create_database: true,
72 alter_table: false,
73 rename_table: false,
74 atomic_batch: true,
75 passwords: false,
76 autonumber: Support::No,
77 relationships: Support::No,
78 native_queries: Support::No,
79 macros: Support::No,
80 vba: Support::No,
81 triggers: Support::No,
82 stored_procedures: Support::No,
83 }
84 }
85
86 fn open(&mut self, path: &Path, _password: Option<&str>) -> Result<OpenedDatabase, PortError> {
87 if !path.is_dir() {
88 return Err(PortError::OpenFailed {
89 detail: format!(
90 "{}: not a CSV directory — this driver opens directories of *.csv tables",
91 path.display()
92 ),
93 });
94 }
95 let tables = collect_tables(path)?;
96 let name = path.file_stem().map_or_else(
97 || "database".to_owned(),
98 |s| s.to_string_lossy().into_owned(),
99 );
100 self.tables = tables;
101 self.dir = Some(path.to_path_buf());
102 Ok(OpenedDatabase {
103 name,
104 format: DatabaseFormat::Csv,
105 lock_file: None,
106 })
107 }
108
109 fn close(&mut self) {
110 self.dir = None;
111 self.tables.clear();
112 }
113
114 fn create_database(&mut self, path: &Path, format: DatabaseFormat) -> Result<(), PortError> {
115 if format != DatabaseFormat::Csv {
116 return Err(PortError::Unsupported {
117 reason: "the CSV driver creates CSV directories only — use the matching \
118 backend for Jet4 or SQLite files"
119 .to_owned(),
120 });
121 }
122 if path.exists() {
123 return Err(PortError::WriteFailed {
124 detail: format!(
125 "{}: already exists — creating would overwrite it",
126 path.display()
127 ),
128 });
129 }
130 std::fs::create_dir_all(path).map_err(|e| PortError::WriteFailed {
131 detail: format!("{}: {e}", path.display()),
132 })
133 }
134
135 fn tables(&self) -> Vec<TableDef> {
136 self.tables.clone()
137 }
138
139 fn create_table(&mut self, definition: &TableDef) -> Result<(), PortError> {
140 let dir = self.dir.as_ref().ok_or_else(no_database)?;
141 definition.validate().map_err(|e| PortError::WriteFailed {
144 detail: format!("table {:?}: {e}", definition.name),
145 })?;
146 let file_name = table_file_name(&definition.name)?;
147 let file = dir.join(file_name);
148 if file.exists() {
149 return Err(PortError::WriteFailed {
150 detail: format!("{}: table already exists", definition.name),
151 });
152 }
153 let header = definition
154 .fields
155 .iter()
156 .map(|f| f.name.clone())
157 .collect::<Vec<String>>();
158 write_rows(&file, &[header])?;
159 self.tables.push(definition.clone());
162 self.tables.sort_by(|a, b| a.name.cmp(&b.name));
163 Ok(())
164 }
165
166 fn drop_table(&mut self, name: &str) -> Result<String, PortError> {
167 let dir = self.dir.as_ref().ok_or_else(no_database)?;
168 let (canonical, _) = self
169 .tables
170 .iter()
171 .find(|def| def.name.eq_ignore_ascii_case(name))
172 .ok_or_else(|| PortError::TableNotFound(name.to_owned()))
173 .map(|def| (def.name.clone(), ()))?;
174 let file = dir.join(table_file_name(&canonical)?);
175 std::fs::remove_file(&file).map_err(|e| PortError::WriteFailed {
176 detail: format!("{}: {e}", file.display()),
177 })?;
178 self.tables.retain(|def| def.name != canonical);
179 Ok(canonical)
180 }
181
182 fn read_rows(&self, table: &str, range: RowRange) -> Result<Vec<Vec<FieldValue>>, PortError> {
183 let rows = self.rows_of(table)?;
184 let (start, end) = window_slice(table, range, rows.len())?;
185 Ok(rows[start..end]
186 .iter()
187 .map(|row| {
188 row.iter()
189 .map(|cell| FieldValue::Text(cell.clone()))
190 .collect()
191 })
192 .collect())
193 }
194
195 fn insert_row(&mut self, table: &str, values: &[FieldValue]) -> Result<(), PortError> {
196 let arity = self.definition_of(table)?.fields.len();
197 let canonical = self.definition_of(table)?.name.clone();
198 if values.len() != arity {
199 return Err(PortError::WriteFailed {
200 detail: format!(
201 "insert into {:?}: expected {} values, found {}",
202 canonical,
203 arity,
204 values.len()
205 ),
206 });
207 }
208 let mut rows = self.rows_of(&canonical)?;
209 rows.push(values.iter().map(to_cell).collect::<Vec<String>>());
210 self.write_table(&canonical, rows)
211 }
212
213 fn update_row(
214 &mut self,
215 table: &str,
216 row: usize,
217 values: &[FieldValue],
218 ) -> Result<(), PortError> {
219 let arity = self.definition_of(table)?.fields.len();
220 let canonical = self.definition_of(table)?.name.clone();
221 if values.len() != arity {
222 return Err(PortError::WriteFailed {
223 detail: format!(
224 "update {:?}: expected {} values, found {}",
225 canonical,
226 arity,
227 values.len()
228 ),
229 });
230 }
231 let mut rows = self.rows_of(&canonical)?;
232 if row >= rows.len() {
233 return Err(out_of_range(&canonical, row, rows.len()));
234 }
235 let target = &mut rows[row];
236 *target = values.iter().map(to_cell).collect::<Vec<String>>();
237 self.write_table(&canonical, rows)
238 }
239
240 fn delete_row(&mut self, table: &str, row: usize) -> Result<(), PortError> {
241 let canonical = self.definition_of(table)?.name.clone();
242 let mut rows = self.rows_of(&canonical)?;
243 if row >= rows.len() {
244 return Err(out_of_range(&canonical, row, rows.len()));
245 }
246 rows.remove(row);
247 self.write_table(&canonical, rows)
248 }
249
250 fn write_row_ops(
254 &mut self,
255 table: &str,
256 ops: &[datacraft_core::port::RowWriteOp],
257 ) -> Result<(), PortError> {
258 let arity = self.definition_of(table)?.fields.len();
259 let canonical = self.definition_of(table)?.name.clone();
260 let mut rows = self.rows_of(&canonical)?;
261 for op in ops {
262 match op {
263 datacraft_core::port::RowWriteOp::Insert(values) => {
264 if values.len() != arity {
265 return Err(PortError::WriteFailed {
266 detail: format!(
267 "batch insert into {:?}: expected {} values, found {}",
268 canonical,
269 arity,
270 values.len()
271 ),
272 });
273 }
274 rows.push(values.iter().map(to_cell).collect::<Vec<String>>());
275 }
276 datacraft_core::port::RowWriteOp::Update { row, values } => {
277 if values.len() != arity {
278 return Err(PortError::WriteFailed {
279 detail: format!(
280 "batch update of {:?}: expected {} values, found {}",
281 canonical,
282 arity,
283 values.len()
284 ),
285 });
286 }
287 if *row >= rows.len() {
288 return Err(out_of_range(&canonical, *row, rows.len()));
289 }
290 let target = &mut rows[*row];
291 *target = values.iter().map(to_cell).collect::<Vec<String>>();
292 }
293 datacraft_core::port::RowWriteOp::Delete { row } => {
294 if *row >= rows.len() {
295 return Err(out_of_range(&canonical, *row, rows.len()));
296 }
297 rows.remove(*row);
298 }
299 }
300 }
301 self.write_table(&canonical, rows)
302 }
303
304 fn relationships(&self) -> Result<Vec<Relationship>, PortError> {
305 Ok(Vec::new())
306 }
307}
308
309impl CsvPort {
310 fn definition_of(&self, table: &str) -> Result<&TableDef, PortError> {
312 self.tables
313 .iter()
314 .find(|def| def.name.eq_ignore_ascii_case(table))
315 .ok_or_else(|| PortError::TableNotFound(table.to_owned()))
316 }
317
318 fn rows_of(&self, table: &str) -> Result<Vec<Vec<String>>, PortError> {
321 let dir = self.dir.as_ref().ok_or_else(no_database)?;
322 let def = self.definition_of(table)?;
323 let file = dir.join(table_file_name(&def.name)?);
324 let records = parse_records(&file, &read_table_text(&file)?)?;
325 Ok(records.into_iter().skip(1).collect())
326 }
327
328 fn write_table(&mut self, table: &str, rows: Vec<Vec<String>>) -> Result<(), PortError> {
330 let dir = self.dir.as_ref().ok_or_else(no_database)?;
331 let def = self.definition_of(table)?.clone();
332 let header: Vec<String> = def.fields.iter().map(|f| f.name.clone()).collect();
333 let file = dir.join(table_file_name(&def.name)?);
334 let mut all = Vec::with_capacity(rows.len() + 1);
335 all.push(header);
336 all.extend(rows);
337 write_rows(&file, &all)
338 }
339}
340
341fn to_cell(value: &FieldValue) -> String {
343 match value {
344 FieldValue::Null => String::new(),
345 FieldValue::Text(text) => text.clone(),
346 other => datacraft_core::present::display_value(other),
347 }
348}
349
350fn table_file_name(name: &str) -> Result<String, PortError> {
353 let ok = !name.is_empty()
354 && name
355 .chars()
356 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-');
357 if !ok {
358 return Err(PortError::WriteFailed {
359 detail: format!(
360 "table name {name:?} is not usable as a CSV file name — use letters, \
361 digits, '_' and '-'"
362 ),
363 });
364 }
365 Ok(format!("{name}.csv"))
366}
367
368fn collect_tables(dir: &Path) -> Result<Vec<TableDef>, PortError> {
370 let mut names: Vec<String> = std::fs::read_dir(dir)
371 .map_err(|e| PortError::OpenFailed {
372 detail: format!("{}: {e}", dir.display()),
373 })?
374 .filter_map(|entry| entry.ok())
375 .filter(|entry| entry.path().is_file())
376 .filter_map(|entry| {
377 let path = entry.path();
378 let stem = path.file_stem()?.to_string_lossy().into_owned();
379 let extension = path.extension()?.to_string_lossy().to_lowercase();
380 (extension == "csv").then_some(stem)
381 })
382 .collect();
383 names.sort();
384 let mut tables = Vec::with_capacity(names.len());
385 for stem in names {
386 let file = dir.join(format!("{stem}.csv"));
387 let text = read_table_text(&file)?;
388 let records = parse_records(&file, &text)?;
389 let header = records.first().ok_or_else(|| PortError::OpenFailed {
390 detail: format!("{}: the CSV file has no header row", file.display()),
391 })?;
392 let mask = quote_mask(&text);
393 let kinds = infer_field_kinds(&records, &mask);
394 let mut fields = Vec::with_capacity(header.len());
395 for (index, column) in header.iter().enumerate() {
396 if column.is_empty() {
397 return Err(PortError::OpenFailed {
398 detail: format!("{}: header column {index} has no name", file.display()),
399 });
400 }
401 fields.push(FieldDef::new(column.clone(), kinds[index]));
402 }
403 tables.push(TableDef::new(stem, fields));
404 }
405 Ok(tables)
406}
407
408fn read_table_text(file: &Path) -> Result<String, PortError> {
412 let bytes = std::fs::read(file).map_err(|e| PortError::OpenFailed {
413 detail: format!("{}: {e}", file.display()),
414 })?;
415 let text = String::from_utf8(bytes).map_err(|e| PortError::OpenFailed {
416 detail: format!("{}: not valid UTF-8: {e}", file.display()),
417 })?;
418 match text.strip_prefix('\u{feff}') {
419 Some(stripped) => Ok(stripped.to_owned()),
420 None => Ok(text),
421 }
422}
423
424fn parse_records(file: &Path, text: &str) -> Result<Vec<Vec<String>>, PortError> {
428 let mut reader = csv::ReaderBuilder::new()
429 .has_headers(false)
430 .flexible(false)
431 .from_reader(text.as_bytes());
432 let mut records: Vec<Vec<String>> = Vec::new();
433 for (index, record) in reader.records().enumerate() {
434 let record = record.map_err(|e| PortError::OpenFailed {
435 detail: format!("{}: row {}: {e}", file.display(), index + 1),
436 })?;
437 records.push(record.into_iter().map(String::from).collect());
440 }
441 Ok(records)
442}
443
444fn write_rows(file: &Path, rows: &[Vec<String>]) -> Result<(), PortError> {
448 let temp = file.with_extension("csv.tmp");
449 let write = || -> Result<(), PortError> {
450 let mut writer = csv::Writer::from_path(&temp).map_err(|e| PortError::WriteFailed {
451 detail: format!("{}: {e}", temp.display()),
452 })?;
453 for row in rows {
454 writer
455 .write_record(row)
456 .map_err(|e| PortError::WriteFailed {
457 detail: format!("{}: {e}", temp.display()),
458 })?;
459 }
460 writer.flush().map_err(|e| PortError::WriteFailed {
461 detail: format!("{}: {e}", temp.display()),
462 })?;
463 std::fs::rename(&temp, file).map_err(|e| PortError::WriteFailed {
464 detail: format!("{} -> {}: {e}", temp.display(), file.display()),
465 })?;
466 Ok(())
467 };
468 let outcome = write();
469 if outcome.is_err() {
470 let _ = std::fs::remove_file(&temp);
471 }
472 outcome
473}
474
475fn out_of_range(table: &str, row: usize, row_count: usize) -> PortError {
476 PortError::OutOfRange {
477 table: table.to_owned(),
478 start: row,
479 len: 1,
480 row_count,
481 }
482}
483
484fn no_database() -> PortError {
485 PortError::Unsupported {
486 reason: "no database is open".to_owned(),
487 }
488}