From 14e3cb6c4a72e53c0b7abd7c776fbd4109587940 Mon Sep 17 00:00:00 2001 From: TimelordUK Date: Tue, 12 Aug 2025 19:06:10 +0100 Subject: [PATCH] feat(v34): introduce DataProvider trait for data abstraction - Create minimal DataProvider trait with core data access methods - Add DataViewProvider trait for filtering/sorting operations - Include default implementations for common methods - Add comprehensive tests for the new traits This is the first step in migrating to a DataTable/DataView architecture. The traits define the contract that the TUI will use to access data, allowing us to gradually replace the underlying implementation. --- sql-cli/src/data_provider.rs | 188 +++++++++++++++++++++++++++++++++++ sql-cli/src/lib.rs | 1 + 2 files changed, 189 insertions(+) create mode 100644 sql-cli/src/data_provider.rs diff --git a/sql-cli/src/data_provider.rs b/sql-cli/src/data_provider.rs new file mode 100644 index 00000000..948c5db8 --- /dev/null +++ b/sql-cli/src/data_provider.rs @@ -0,0 +1,188 @@ +//! Data provider traits for abstracting data access +//! +//! This module defines the core traits that allow the TUI to work with +//! data without knowing the underlying implementation (Buffer, CSVClient, DataTable, etc.) + +use std::fmt::Debug; + +/// Core trait for read-only data access +/// +/// This trait defines the minimal interface that any data source must provide +/// to be usable by the TUI for rendering and display. +pub trait DataProvider: Send + Sync + Debug { + /// Get a single row by index + /// Returns None if the index is out of bounds + fn get_row(&self, index: usize) -> Option>; + + /// Get the column names/headers + fn get_column_names(&self) -> Vec; + + /// Get the total number of rows + fn get_row_count(&self) -> usize; + + /// Get the total number of columns + fn get_column_count(&self) -> usize; + + /// Get multiple rows for efficient rendering + /// This is an optimization to avoid multiple get_row calls + fn get_visible_rows(&self, start: usize, count: usize) -> Vec> { + let mut rows = Vec::new(); + let end = (start + count).min(self.get_row_count()); + + for i in start..end { + if let Some(row) = self.get_row(i) { + rows.push(row); + } + } + + rows + } + + /// Get the display width for each column + /// Used for rendering column widths in the TUI + fn get_column_widths(&self) -> Vec { + // Default implementation: calculate from first 100 rows + let mut widths = vec![0; self.get_column_count()]; + let sample_size = 100.min(self.get_row_count()); + + // Start with column name widths + for (i, name) in self.get_column_names().iter().enumerate() { + if i < widths.len() { + widths[i] = name.len(); + } + } + + // Check first 100 rows for max width + for row_idx in 0..sample_size { + if let Some(row) = self.get_row(row_idx) { + for (col_idx, value) in row.iter().enumerate() { + if col_idx < widths.len() { + widths[col_idx] = widths[col_idx].max(value.len()); + } + } + } + } + + widths + } + + /// Get a single cell value + /// Returns None if row or column index is out of bounds + fn get_cell_value(&self, row: usize, col: usize) -> Option { + self.get_row(row).and_then(|r| r.get(col).cloned()) + } + + /// Get a display-formatted cell value + /// Returns empty string if indices are out of bounds + fn get_display_value(&self, row: usize, col: usize) -> String { + self.get_cell_value(row, col).unwrap_or_default() + } +} + +/// Extended trait for data views that support filtering and sorting +/// +/// This trait extends DataProvider with mutable operations that change +/// what data is visible without modifying the underlying data. +pub trait DataViewProvider: DataProvider { + /// Apply a filter to the view + /// The filter string format depends on the implementation + fn apply_filter(&mut self, filter: &str) -> Result<(), String>; + + /// Clear all filters + fn clear_filters(&mut self); + + /// Get the number of rows after filtering + fn get_filtered_count(&self) -> usize { + // Default: same as total count (no filtering) + self.get_row_count() + } + + /// Sort by a column + fn sort_by(&mut self, column_index: usize, ascending: bool) -> Result<(), String>; + + /// Clear sorting and return to original order + fn clear_sort(&mut self); + + /// Check if a row index is visible in the current view + fn is_row_visible(&self, row_index: usize) -> bool { + row_index < self.get_row_count() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Mock implementation for testing + #[derive(Debug)] + struct MockDataProvider { + columns: Vec, + rows: Vec>, + } + + impl DataProvider for MockDataProvider { + fn get_row(&self, index: usize) -> Option> { + self.rows.get(index).cloned() + } + + fn get_column_names(&self) -> Vec { + self.columns.clone() + } + + fn get_row_count(&self) -> usize { + self.rows.len() + } + + fn get_column_count(&self) -> usize { + self.columns.len() + } + } + + #[test] + fn test_data_provider_basics() { + let provider = MockDataProvider { + columns: vec!["ID".to_string(), "Name".to_string(), "Age".to_string()], + rows: vec![ + vec!["1".to_string(), "Alice".to_string(), "30".to_string()], + vec!["2".to_string(), "Bob".to_string(), "25".to_string()], + ], + }; + + assert_eq!(provider.get_row_count(), 2); + assert_eq!(provider.get_column_count(), 3); + assert_eq!(provider.get_column_names(), vec!["ID", "Name", "Age"]); + assert_eq!( + provider.get_row(0), + Some(vec!["1".to_string(), "Alice".to_string(), "30".to_string()]) + ); + assert_eq!(provider.get_cell_value(1, 1), Some("Bob".to_string())); + } + + #[test] + fn test_get_visible_rows() { + let provider = MockDataProvider { + columns: vec!["Col1".to_string()], + rows: (0..10).map(|i| vec![format!("Row{}", i)]).collect(), + }; + + let visible = provider.get_visible_rows(2, 3); + assert_eq!(visible.len(), 3); + assert_eq!(visible[0], vec!["Row2"]); + assert_eq!(visible[2], vec!["Row4"]); + } + + #[test] + fn test_column_widths() { + let provider = MockDataProvider { + columns: vec!["ID".to_string(), "LongColumnName".to_string()], + rows: vec![ + vec!["123456".to_string(), "Short".to_string()], + vec!["1".to_string(), "Value".to_string()], + ], + }; + + let widths = provider.get_column_widths(); + assert_eq!(widths[0], 6); // "123456" is longest + assert_eq!(widths[1], 14); // "LongColumnName" is longest + } +} diff --git a/sql-cli/src/lib.rs b/sql-cli/src/lib.rs index 47f8aa6a..a5dd36e6 100644 --- a/sql-cli/src/lib.rs +++ b/sql-cli/src/lib.rs @@ -10,6 +10,7 @@ pub mod csv_fixes; pub mod cursor_aware_parser; pub mod cursor_operations; pub mod data_exporter; +pub mod data_provider; pub mod datasource_adapter; pub mod datasource_trait; pub mod datatable;