use super::*;
use crate::app::modals::filter_modal::{FilterOperator, LogicalOperator};
fn state() -> DataTableState {
let lf = df!(
"id" => (0..20i64).collect::<Vec<_>>(),
"key" => (0..20).map(|i| if i % 2 == 0 { "a" } else { "b" }).collect::<Vec<_>>(),
"val" => (0..20i64).map(|i| i * 10).collect::<Vec<_>>(),
)
.unwrap()
.lazy();
let mut state = DataTableState::from_lazyframe(lf, &crate::OpenOptions::default()).unwrap();
state.visible_rows = 4;
state.collect();
state
}
fn filter(column: &str, op: FilterOperator, value: &str) -> FilterStatement {
FilterStatement {
columns: Vec::new(),
column: column.to_string(),
operator: op,
value: value.to_string(),
logical_op: LogicalOperator::And,
}
}
fn busy_view() -> DataTableState {
let mut state = state();
state.query("select id, val, key where val >= 20".to_string());
state.filter(vec![filter("val", FilterOperator::Lt, "170")]);
state.sort_by(vec!["val".to_string()], vec![true]);
state.set_column_order(vec!["val".to_string(), "id".to_string(), "key".to_string()]);
state.set_locked_columns(1);
state.scroll_to(3);
state.collect();
state.table_state.select(Some(2));
assert!(state.error().is_none(), "{:?}", state.error());
assert!(state.is_num_rows_valid());
assert!(state.display_df().is_some());
state
}
fn melted_view() -> DataTableState {
let mut state = state();
state
.melt(&MeltSpec {
index: vec!["id".to_string()],
value_columns: vec!["val".to_string()],
variable_name: "variable".to_string(),
value_name: "value".to_string(),
})
.unwrap();
state.filter(vec![filter("id", FilterOperator::Gt, "3")]);
state.collect();
state.table_state.select(Some(1));
assert!(state.error().is_none(), "{:?}", state.error());
state
}
fn planned(state: &mut DataTableState) -> std::result::Result<(), String> {
if let Some(e) = state.error() {
return Err(e.to_string());
}
state.check_plan().map_err(|e| e.to_string())
}
#[test]
fn a_transition_failing_after_each_step_puts_the_view_back() {
type Steps = fn(&mut DataTableState) -> std::result::Result<(), String>;
let cases: [(&str, Steps); 8] = [
("the query", |s| {
s.query("select nope".to_string());
planned(s)
}),
("a filter after the query", |s| {
s.query("select id, val".to_string());
s.filter(vec![filter("key", FilterOperator::Eq, "a")]);
planned(s)
}),
("a sort after the filter", |s| {
s.query("select id, val".to_string());
s.filter(vec![filter("val", FilterOperator::Gt, "0")]);
s.sort_by(vec!["key".to_string()], vec![false]);
planned(s)
}),
("a melt after the query", |s| {
s.query("select id, val".to_string());
s.melt(&MeltSpec {
index: vec!["id".to_string()],
value_columns: vec!["nope".to_string()],
variable_name: "variable".to_string(),
value_name: "value".to_string(),
})
.map_err(|e| e.to_string())?;
planned(s)
}),
("a filter after the melt", |s| {
s.melt(&MeltSpec {
index: vec!["id".to_string()],
value_columns: vec!["val".to_string()],
variable_name: "variable".to_string(),
value_name: "value".to_string(),
})
.map_err(|e| e.to_string())?;
s.filter(vec![filter("val", FilterOperator::Gt, "0")]);
planned(s)
}),
("a sort after the melt", |s| {
s.melt(&MeltSpec {
index: vec!["id".to_string()],
value_columns: vec!["val".to_string()],
variable_name: "variable".to_string(),
value_name: "value".to_string(),
})
.map_err(|e| e.to_string())?;
s.sort_by(vec!["key".to_string()], vec![false]);
planned(s)
}),
("the layout after the sort", |s| {
s.query("select id, val".to_string());
s.sort_by(vec!["val".to_string()], vec![false]);
s.set_column_order(vec!["key".to_string()]);
planned(s)
}),
("a reset, then a query", |s| {
s.reset();
s.query("select id where nope > 1".to_string());
planned(s)
}),
];
for prior in [busy_view as fn() -> DataTableState, melted_view] {
for (step, steps) in cases {
let mut state = prior();
let before = state.snapshot();
let failed = state.try_transition(steps);
assert!(failed.is_err(), "{step}: the steps fail");
assert_eq!(state.snapshot(), before, "{step}: the view is put back");
assert!(
state.prepare_async_collect(None).is_none(),
"{step}: the rows on hand serve it, so nothing is read again"
);
}
}
}
#[test]
fn a_planned_view_whose_rows_fail_rolls_back_without_reading() {
for prior in [busy_view as fn() -> DataTableState, melted_view] {
let mut state = prior();
let before = state.snapshot();
let ((), saved) = state
.try_transition(|s| {
s.query("select id, val where id > 4".to_string());
s.filter(vec![filter("val", FilterOperator::Lt, "150")]);
s.sort_by(vec!["id".to_string()], vec![false]);
planned(s)
})
.unwrap();
assert!(!state.is_num_rows_valid(), "the view's count is not read");
assert!(
state.prepare_async_collect(None).is_some(),
"nor its rows: both are left to the background read"
);
state.roll_back(saved);
assert_eq!(state.snapshot(), before);
}
}
#[test]
fn a_count_that_lands_meanwhile_comes_back_with_its_view() {
let mut state = busy_view();
state.invalidate_num_rows();
let counting = state.len_generation();
let ((), mut saved) = state
.try_transition(|s| {
s.query("select id".to_string());
planned(s)
})
.unwrap();
assert!(
!state.count_landed(counting, 7, None),
"not a count of the frame on screen"
);
assert!(
!saved.count_landed(state.len_generation(), 99, None),
"nor is the query's count the view's"
);
assert!(saved.count_landed(counting, 7, None));
state.roll_back(saved);
assert_eq!(state.len_generation(), counting);
assert_eq!(state.num_rows_if_valid(), Some(7));
}
#[test]
fn a_checkpoint_over_replaced_data_returns_to_the_data() {
let mut state = busy_view();
let saved = state.rollback_point();
let wider = df!(
"id" => &[1i64, 2],
"key" => &["a", "b"],
"val" => &[10i64, 20],
"more" => &[true, false],
)
.unwrap()
.lazy();
let schema = wider.clone().collect_schema().unwrap();
state.replace_root(wider.clone(), schema.clone());
state.roll_back(saved);
assert_eq!(state.schema(), &schema);
assert!(state.get_active_query().is_empty());
assert!(state.get_filters().is_empty());
assert!(state.get_sort_columns().is_empty());
assert_eq!(
state.lf().clone().collect().unwrap(),
wider.collect().unwrap()
);
assert!(
state.prepare_async_collect(None).is_some(),
"its rows are read in the background"
);
}