1use std::collections::HashMap;
8use std::fs;
9use std::io::{BufRead, BufReader, Write as IoWrite};
10use std::path::{Path, PathBuf};
11
12use super::log::TransactionLog;
13use super::types::{
14 ColumnSchema, DeltaAction, DeltaConfig, DeltaError, DeltaTable, DeltaTransaction, DeltaVersion,
15 FileInfo, Schema,
16};
17
18pub struct DeltaTableWriter {
23 config: DeltaConfig,
24 log: TransactionLog,
25}
26
27impl DeltaTableWriter {
28 pub fn new(config: DeltaConfig) -> Result<Self, DeltaError> {
30 let log = TransactionLog::new(&config.base_path)?;
31 let data_dir = config.base_path.join("data");
33 fs::create_dir_all(&data_dir)?;
34 Ok(Self { config, log })
35 }
36
37 pub fn write(&self, data: &[Vec<f64>], schema: &Schema) -> Result<u64, DeltaError> {
44 if data.is_empty() {
45 return Err(DeltaError::Other("cannot write empty data".to_string()));
46 }
47
48 if data.len() != schema.columns.len() {
50 return Err(DeltaError::SchemaError(format!(
51 "data has {} columns but schema has {}",
52 data.len(),
53 schema.columns.len()
54 )));
55 }
56
57 let next_version = self.log.latest_version().map(|v| v + 1).unwrap_or(0);
58
59 let file_name = format!("part-{next_version:05}.csv");
61 let file_path = self.config.base_path.join("data").join(&file_name);
62
63 write_data_file(&file_path, data, schema)?;
64
65 let file_size = fs::metadata(&file_path).map(|m| m.len()).unwrap_or(0);
66
67 let now_ms = std::time::SystemTime::now()
68 .duration_since(std::time::UNIX_EPOCH)
69 .map(|d| d.as_millis() as u64)
70 .unwrap_or(0);
71
72 let mut actions = Vec::new();
74
75 if next_version == 0 {
77 actions.push(DeltaAction::Protocol {
78 min_reader_version: 1,
79 min_writer_version: 1,
80 });
81 let schema_json = schema.to_json().unwrap_or_else(|_| "{}".to_string());
82 actions.push(DeltaAction::Metadata {
83 schema: schema_json,
84 partition_columns: Vec::new(),
85 description: None,
86 configuration: HashMap::new(),
87 });
88 }
89
90 actions.push(DeltaAction::Add {
91 path: format!("data/{file_name}"),
92 size: file_size,
93 modification_time: now_ms,
94 data_change: true,
95 partition_values: HashMap::new(),
96 stats_json: None,
97 });
98
99 actions.push(DeltaAction::CommitInfo {
100 timestamp: now_ms as i64,
101 operation: "WRITE".to_string(),
102 operation_parameters: HashMap::new(),
103 });
104
105 let committed = self.log.commit(actions)?;
106
107 if self.config.checkpoint_interval > 0
109 && committed > 0
110 && (committed as usize).is_multiple_of(self.config.checkpoint_interval)
111 {
112 let _ = self.log.checkpoint(committed);
114 }
115
116 Ok(committed)
117 }
118
119 pub fn write_partitioned(
125 &self,
126 data: &[Vec<f64>],
127 schema: &Schema,
128 partition_column: &str,
129 ) -> Result<u64, DeltaError> {
130 if data.is_empty() || schema.columns.is_empty() {
131 return Err(DeltaError::Other("cannot write empty data".to_string()));
132 }
133
134 let part_idx = schema
136 .columns
137 .iter()
138 .position(|c| c.name == partition_column)
139 .ok_or_else(|| {
140 DeltaError::SchemaError(format!(
141 "partition column '{partition_column}' not found in schema"
142 ))
143 })?;
144
145 let num_rows = data[0].len();
146 for col in data {
147 if col.len() != num_rows {
148 return Err(DeltaError::SchemaError(
149 "all columns must have the same number of rows".to_string(),
150 ));
151 }
152 }
153
154 let mut partitions: HashMap<String, Vec<usize>> = HashMap::new();
156 for row_idx in 0..num_rows {
157 let val = data[part_idx][row_idx];
158 let key = format!("{val}");
159 partitions.entry(key).or_default().push(row_idx);
160 }
161
162 let next_version = self.log.latest_version().map(|v| v + 1).unwrap_or(0);
163 let now_ms = std::time::SystemTime::now()
164 .duration_since(std::time::UNIX_EPOCH)
165 .map(|d| d.as_millis() as u64)
166 .unwrap_or(0);
167
168 let mut actions = Vec::new();
169
170 if next_version == 0 {
171 actions.push(DeltaAction::Protocol {
172 min_reader_version: 1,
173 min_writer_version: 1,
174 });
175 let schema_json = schema.to_json().unwrap_or_else(|_| "{}".to_string());
176 actions.push(DeltaAction::Metadata {
177 schema: schema_json,
178 partition_columns: vec![partition_column.to_string()],
179 description: None,
180 configuration: HashMap::new(),
181 });
182 }
183
184 let mut part_keys: Vec<String> = partitions.keys().cloned().collect();
185 part_keys.sort();
186
187 for (file_idx, part_val) in part_keys.iter().enumerate() {
188 let row_indices = &partitions[part_val];
189
190 let part_data: Vec<Vec<f64>> = data
192 .iter()
193 .map(|col| row_indices.iter().map(|&ri| col[ri]).collect())
194 .collect();
195
196 let part_dir = self
198 .config
199 .base_path
200 .join("data")
201 .join(format!("{partition_column}={part_val}"));
202 fs::create_dir_all(&part_dir)?;
203
204 let file_name = format!("part-{next_version:05}-{file_idx:03}.csv");
205 let file_path = part_dir.join(&file_name);
206 write_data_file(&file_path, &part_data, schema)?;
207
208 let file_size = fs::metadata(&file_path).map(|m| m.len()).unwrap_or(0);
209
210 let mut pv = HashMap::new();
211 pv.insert(partition_column.to_string(), part_val.clone());
212
213 actions.push(DeltaAction::Add {
214 path: format!("data/{partition_column}={part_val}/{file_name}"),
215 size: file_size,
216 modification_time: now_ms,
217 data_change: true,
218 partition_values: pv,
219 stats_json: None,
220 });
221 }
222
223 actions.push(DeltaAction::CommitInfo {
224 timestamp: now_ms as i64,
225 operation: "WRITE".to_string(),
226 operation_parameters: HashMap::new(),
227 });
228
229 self.log.commit(actions)
230 }
231
232 pub fn transaction_log(&self) -> &TransactionLog {
234 &self.log
235 }
236}
237
238pub struct DeltaTableReader {
240 config: DeltaConfig,
241 log: TransactionLog,
242}
243
244impl DeltaTableReader {
245 pub fn new(config: DeltaConfig) -> Result<Self, DeltaError> {
247 let log = TransactionLog::new(&config.base_path)?;
248 Ok(Self { config, log })
249 }
250
251 pub fn read(&self, version: Option<u64>) -> Result<Vec<Vec<f64>>, DeltaError> {
255 let files = self.log.reconstruct_files(version)?;
256 if files.is_empty() {
257 return Ok(Vec::new());
258 }
259
260 self.read_files(&files)
261 }
262
263 pub fn read_pruned(
268 &self,
269 version: Option<u64>,
270 partitions: &HashMap<String, String>,
271 ) -> Result<Vec<Vec<f64>>, DeltaError> {
272 let all_files = self.log.reconstruct_files(version)?;
273
274 let filtered: HashMap<String, FileInfo> = all_files
276 .into_iter()
277 .filter(|(_, info)| {
278 partitions
279 .iter()
280 .all(|(col, val)| info.partition_values.get(col).map_or(true, |fv| fv == val))
281 })
282 .collect();
283
284 if filtered.is_empty() {
285 return Ok(Vec::new());
286 }
287
288 self.read_files(&filtered)
289 }
290
291 pub fn history(&self) -> Result<Vec<DeltaVersion>, DeltaError> {
293 self.log.history()
294 }
295
296 pub fn latest_version(&self) -> Option<u64> {
298 self.log.latest_version()
299 }
300
301 pub fn table_state(&self, version: Option<u64>) -> Result<DeltaTable, DeltaError> {
303 let target_version = version.or_else(|| self.log.latest_version()).unwrap_or(0);
304 let actions = self.log.replay_up_to(target_version)?;
305
306 let mut active_files = HashMap::new();
307 let mut schema: Option<Schema> = None;
308 let mut partition_columns = Vec::new();
309 let mut protocol: Option<(u32, u32)> = None;
310
311 for action in &actions {
312 match action {
313 DeltaAction::Add {
314 path,
315 size,
316 modification_time,
317 partition_values,
318 ..
319 } => {
320 active_files.insert(
321 path.clone(),
322 FileInfo {
323 path: path.clone(),
324 size: *size,
325 modification_time: *modification_time,
326 partition_values: partition_values.clone(),
327 },
328 );
329 }
330 DeltaAction::Metadata {
331 schema: schema_json,
332 partition_columns: pc,
333 ..
334 } => {
335 schema = Schema::from_json(schema_json).ok();
336 partition_columns = pc.clone();
337 }
338 DeltaAction::Protocol {
339 min_reader_version,
340 min_writer_version,
341 } => {
342 protocol = Some((*min_reader_version, *min_writer_version));
343 }
344 _ => {}
345 }
346 }
347
348 Ok(DeltaTable {
349 config: self.config.clone(),
350 version: target_version,
351 active_files,
352 schema,
353 partition_columns,
354 protocol,
355 })
356 }
357
358 fn read_files(&self, files: &HashMap<String, FileInfo>) -> Result<Vec<Vec<f64>>, DeltaError> {
360 let mut all_columns: Vec<Vec<f64>> = Vec::new();
361 let mut first = true;
362
363 let mut sorted_paths: Vec<&String> = files.keys().collect();
364 sorted_paths.sort();
365
366 for file_path_rel in sorted_paths {
367 let abs_path = self.config.base_path.join(file_path_rel);
368 if !abs_path.exists() {
369 continue;
370 }
371
372 let file_data = read_data_file(&abs_path)?;
373 if file_data.is_empty() {
374 continue;
375 }
376
377 if first {
378 all_columns = file_data;
379 first = false;
380 } else {
381 if all_columns.len() != file_data.len() {
383 return Err(DeltaError::SchemaError(format!(
384 "column count mismatch: {} vs {}",
385 all_columns.len(),
386 file_data.len()
387 )));
388 }
389 for (i, col) in file_data.into_iter().enumerate() {
390 all_columns[i].extend(col);
391 }
392 }
393 }
394
395 Ok(all_columns)
396 }
397
398 pub fn transaction_log(&self) -> &TransactionLog {
400 &self.log
401 }
402}
403
404pub fn add_column(
412 log: &TransactionLog,
413 current_schema: &Schema,
414 column: ColumnSchema,
415) -> Result<u64, DeltaError> {
416 let mut new_schema = current_schema.clone();
417 if new_schema.columns.iter().any(|c| c.name == column.name) {
418 return Err(DeltaError::SchemaError(format!(
419 "column '{}' already exists",
420 column.name
421 )));
422 }
423 new_schema.columns.push(column);
424
425 let schema_json = new_schema.to_json()?;
426
427 let now_ms = std::time::SystemTime::now()
428 .duration_since(std::time::UNIX_EPOCH)
429 .map(|d| d.as_millis() as i64)
430 .unwrap_or(0);
431
432 let actions = vec![
433 DeltaAction::Metadata {
434 schema: schema_json,
435 partition_columns: Vec::new(),
436 description: Some("schema evolution: add column".to_string()),
437 configuration: HashMap::new(),
438 },
439 DeltaAction::CommitInfo {
440 timestamp: now_ms,
441 operation: "ALTER_TABLE".to_string(),
442 operation_parameters: HashMap::new(),
443 },
444 ];
445
446 log.commit(actions)
447}
448
449pub fn rename_column(
454 log: &TransactionLog,
455 current_schema: &Schema,
456 old_name: &str,
457 new_name: &str,
458) -> Result<u64, DeltaError> {
459 let mut new_schema = current_schema.clone();
460 let col = new_schema
461 .columns
462 .iter_mut()
463 .find(|c| c.name == old_name)
464 .ok_or_else(|| DeltaError::SchemaError(format!("column '{old_name}' not found")))?;
465 col.name = new_name.to_string();
466
467 let schema_json = new_schema.to_json()?;
468
469 let now_ms = std::time::SystemTime::now()
470 .duration_since(std::time::UNIX_EPOCH)
471 .map(|d| d.as_millis() as i64)
472 .unwrap_or(0);
473
474 let actions = vec![
475 DeltaAction::Metadata {
476 schema: schema_json,
477 partition_columns: Vec::new(),
478 description: Some(format!("schema evolution: rename {old_name} -> {new_name}")),
479 configuration: HashMap::new(),
480 },
481 DeltaAction::CommitInfo {
482 timestamp: now_ms,
483 operation: "ALTER_TABLE".to_string(),
484 operation_parameters: HashMap::new(),
485 },
486 ];
487
488 log.commit(actions)
489}
490
491fn write_data_file(path: &Path, data: &[Vec<f64>], schema: &Schema) -> Result<(), DeltaError> {
495 let mut file = fs::File::create(path)?;
496
497 let header: Vec<&str> = schema.columns.iter().map(|c| c.name.as_str()).collect();
499 writeln!(file, "{}", header.join(","))
500 .map_err(|e| DeltaError::Io(std::io::Error::new(e.kind(), format!("write header: {e}"))))?;
501
502 let num_rows = data.first().map(|c| c.len()).unwrap_or(0);
504 for row_idx in 0..num_rows {
505 let row: Vec<String> = data
506 .iter()
507 .map(|col| {
508 if row_idx < col.len() {
509 format!("{}", col[row_idx])
510 } else {
511 "NaN".to_string()
512 }
513 })
514 .collect();
515 writeln!(file, "{}", row.join(",")).map_err(|e| {
516 DeltaError::Io(std::io::Error::new(e.kind(), format!("write row: {e}")))
517 })?;
518 }
519
520 file.flush()?;
521 Ok(())
522}
523
524fn read_data_file(path: &Path) -> Result<Vec<Vec<f64>>, DeltaError> {
526 let file = fs::File::open(path)?;
527 let reader = BufReader::new(file);
528
529 let mut lines = reader.lines();
530
531 let header_line = lines
533 .next()
534 .ok_or_else(|| DeltaError::Parse("empty data file".to_string()))?
535 .map_err(|e| DeltaError::Io(e))?;
536 let num_cols = header_line.split(',').count();
537
538 let mut columns: Vec<Vec<f64>> = (0..num_cols).map(|_| Vec::new()).collect();
539
540 for line_res in lines {
541 let line = line_res?;
542 let trimmed = line.trim();
543 if trimmed.is_empty() {
544 continue;
545 }
546 let fields: Vec<&str> = trimmed.split(',').collect();
547 for (i, field) in fields.iter().enumerate() {
548 if i < num_cols {
549 let val: f64 = field.trim().parse().unwrap_or(f64::NAN);
550 columns[i].push(val);
551 }
552 }
553 for col in columns.iter_mut().skip(fields.len()) {
555 col.push(f64::NAN);
556 }
557 }
558
559 Ok(columns)
560}