From 78ade3ec98133a9f137f01cea0c302046d79b839 Mon Sep 17 00:00:00 2001 From: Michael Cuffaro Date: Sat, 12 Oct 2024 19:10:31 -0400 Subject: [PATCH] add cli history --- src/main.rs | 157 +++++++++++++------- src/toolkit.rs | 299 ++++++++++++++++++++------------------ src/valve.rs | 212 +++++++++++++++++++++------ test/expected/history.tsv | 4 +- 4 files changed, 426 insertions(+), 246 deletions(-) diff --git a/src/main.rs b/src/main.rs index 98ac21b..b88f892 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,10 @@ use clap::{ArgAction, Parser, Subcommand}; use futures::{executor::block_on, TryStreamExt}; use ontodev_valve::{ guess::guess, - toolkit::{any_row_to_json_row, generic_select_with_message_values, local_sql_syntax}, + toolkit::{ + convert_undo_or_redo_record_to_change, generic_select_with_message_values, + get_db_records_to_redo, local_sql_syntax, + }, valve::{JsonRow, Valve, ValveCell, ValveRow}, SQL_PARAM, }; @@ -146,6 +149,9 @@ enum Commands { /// Redo the last row change Redo {}, + /// TODO: Add docstring + History {}, + /// Print the Valve configuration as a JSON-formatted string. DumpConfig {}, @@ -343,6 +349,7 @@ enum DeleteSubcommands { rows: Vec, }, + // TODO: Allow for syntax: ./valve delete messages SQL_LIKE_STRING /// TODO: Add docstring Message { #[arg(value_name = "MESSAGE_ID", action = ArgAction::Set, @@ -629,37 +636,6 @@ async fn main() -> Result<()> { Commands::Get { get_subcommand } => { let valve = build_valve(&cli.source, &cli.database).expect(BUILD_ERROR); match get_subcommand { - GetSubcommands::Table { table } => { - let (sql, sql_params) = - generic_select_with_message_values(table, &valve.config, &valve.db_kind); - let sql = local_sql_syntax(&valve.db_kind, &sql); - let mut query = sqlx_query(&sql); - for param in &sql_params { - query = query.bind(param); - } - - let mut row_stream = query.fetch(&valve.pool); - let mut is_first = true; - print!("["); - while let Some(row) = row_stream.try_next().await? { - if !is_first { - print!(","); - } else { - is_first = false; - } - let row_number: i64 = row.get::("row_number"); - let row_number = row_number as u32; - let row = any_row_to_json_row(&row).unwrap(); - let row = ValveRow::from_rich_json(Some(row_number), &row).unwrap(); - print!( - "{}", - json!(row - .to_rich_json() - .expect("Error converting row to rich JSON")) - ); - } - println!("]"); - } GetSubcommands::Row { table, row } => { let row = valve .get_row_from_db(table, row) @@ -691,20 +667,55 @@ async fn main() -> Result<()> { .expect("Error getting cell"); println!("{}", cell.strvalue()); } + GetSubcommands::Table { table } => { + let (sql, sql_params) = + generic_select_with_message_values(table, &valve.config, &valve.db_kind); + let sql = local_sql_syntax(&valve.db_kind, &sql); + let mut query = sqlx_query(&sql); + for param in &sql_params { + query = query.bind(param); + } + + let mut row_stream = query.fetch(&valve.pool); + let mut is_first = true; + print!("["); + while let Some(row) = row_stream.try_next().await? { + if !is_first { + print!(","); + } else { + is_first = false; + } + let row = ValveRow::from_any_row( + &valve.config, + &valve.db_kind, + table, + &row, + &None, + ) + .expect("Error converting to ValveRow"); + println!( + "{}", + json!(row + .to_rich_json() + .expect("Error converting row to rich JSON")) + ); + } + println!("]"); + } GetSubcommands::Messages { table, row, column } => { let mut sql = format!( r#"SELECT "row", "column", "value", "level", "rule", "message" FROM "message" WHERE "table" = {SQL_PARAM}"# ); - let mut params = vec![table]; + let mut sql_params = vec![table]; match row { Some(row) => { sql.push_str(&format!(r#" AND "row" = {row}"#)); match column { Some(column) => { sql.push_str(&format!(r#" AND "column" = {SQL_PARAM}"#)); - params.push(column); + sql_params.push(column); } None => (), } @@ -716,28 +727,33 @@ async fn main() -> Result<()> { &format!(r#"{sql} ORDER BY "row", "column", "message_id""#,), ); let mut query = sqlx_query(&sql); - for param in ¶ms { + for param in &sql_params { query = query.bind(param); } - let rows = query.fetch_all(&valve.pool).await?; - let messages = rows - .iter() - .map(|row| { - let rn: i64 = row.get::("row"); - let rn = rn as u32; - format!( - "{{\"row\":\"{}\",\"column\":\"{}\",\"value\":\"{}\",\ - \"level\":\"{}\",\"rule\":\"{}\",\"message\":\"{}\"}}", - rn, - row.get::<&str, _>("column"), - row.get::<&str, _>("value"), - row.get::<&str, _>("level"), - row.get::<&str, _>("rule"), - row.get::<&str, _>("message"), - ) - }) - .collect::>(); - println!("[{}]", messages.join(",")); + + let mut row_stream = query.fetch(&valve.pool); + let mut is_first = true; + print!("["); + while let Some(row) = row_stream.try_next().await? { + if !is_first { + print!(","); + } else { + is_first = false; + } + let rn: i64 = row.get::("row"); + let rn = rn as u32; + println!( + "{{\"row\":{},\"column\":{},\"value\":{},\ + \"level\":{},\"rule\":{},\"message\":{}}}", + rn, + json!(row.get::<&str, _>("column")), + json!(row.get::<&str, _>("value")), + json!(row.get::<&str, _>("level")), + json!(row.get::<&str, _>("rule")), + json!(row.get::<&str, _>("message")), + ); + } + println!("]"); } }; } @@ -758,6 +774,39 @@ async fn main() -> Result<()> { cli.assume_yes, ); } + Commands::History {} => { + let valve = build_valve(&cli.source, &cli.database).expect(BUILD_ERROR); + let redo_history = get_db_records_to_redo(&valve.pool, 1).await?; + let next_redo = match redo_history.len() { + 0 => 0, + 1 => { + let history_id = redo_history[0] + .try_get::("history_id") + .expect("No 'history_id' in row"); + history_id as u16 + } + _ => panic!("Too many redos"), + }; + if next_redo != 0 { + let last_undo = convert_undo_or_redo_record_to_change(&redo_history[0])?; + println!("+ {}", last_undo.message); + }; + + let undo_history = valve.get_changes_to_undo(0).await?; + let next_undo = match undo_history.len() { + 0 => 0, + _ => undo_history[0].history_id, + }; + + for undo in &undo_history { + let marker = if undo.history_id == next_undo { + "* " + } else { + " " + }; + println!("{}{} {}", marker, undo.history_id, undo.message); + } + } Commands::Load { initial_load } => { let mut valve = build_valve(&cli.source, &cli.database).expect(BUILD_ERROR); if *initial_load { diff --git a/src/toolkit.rs b/src/toolkit.rs index 124fe0c..cfb27ab 100644 --- a/src/toolkit.rs +++ b/src/toolkit.rs @@ -2767,62 +2767,10 @@ pub async fn get_text_row_from_db_tx( .into()); } let any_row = &rows[0]; - Ok(any_row_to_json_row(any_row)?) -} - -/// TODO: Add a docstring -pub fn any_row_to_json_row(any_row: &AnyRow) -> Result { - let messages = { - let raw_messages = any_row.try_get_raw("message")?; - if raw_messages.is_null() { - vec![] - } else { - let messages: &str = any_row.get("message"); - match serde_json::from_str::(messages) { - Err(e) => return Err(e.into()), - Ok(SerdeValue::Array(m)) => m, - _ => { - return Err(ValveError::DataError( - format!("{} is not an array.", messages).into(), - ) - .into()) - } - } - } - }; - - let mut row = SerdeMap::new(); - for column in any_row.columns() { - let cname = column.name(); - if !vec!["row_number", "message"].contains(&cname) { - let raw_value = any_row.try_get_raw(format!(r#"{}"#, cname).as_str())?; - let value; - if !raw_value.is_null() { - // The extended query returned by generic_select_with_message_values() casts all - // column values to text, so we pass "text" to get_column_value() for every column: - value = get_column_value_as_string(&any_row, &cname, "text"); - } else { - value = String::from(""); - } - - let column_messages = messages - .iter() - .filter(|m| m.get("column").unwrap().as_str() == Some(cname)) - .collect::>(); - let valid = column_messages - .iter() - .filter(|m| m.get("level").unwrap().as_str() == Some("error")) - .collect::>() - .is_empty(); - let cell = json!({ - "value": value, - "valid": valid, - "messages": column_messages, - }); - row.insert(cname.to_string(), json!(cell)); - } - } - Ok(row) + // The extended query returned by generic_select_with_message_values() casts all + // column values to text, so we use "text" as the global sql type for the row's columns: + let valve_row = ValveRow::from_any_row(config, kind, table, any_row, &Some("text"))?; + valve_row.contents_to_rich_json() } /// TODO: Add a docstring @@ -2876,8 +2824,8 @@ pub fn correct_row_datatypes( Ok(corrected_row) } -/// Given a row and a column name, extract the contents of the row as a JSON array and return it. -pub fn get_json_array_from_row(row: &AnyRow, column: &str) -> Option> { +/// Given a row and a column name, return the contents of the row as a JSON array. +pub fn get_json_array_from_column(row: &AnyRow, column: &str) -> Option> { let raw_value = row .try_get_raw(column) .expect("Unable to get raw value from row"); @@ -2899,8 +2847,8 @@ pub fn get_json_array_from_row(row: &AnyRow, column: &str) -> Option Option { +/// Given a row and a column name, return the contents of the column as a JSON object. +pub fn get_json_object_from_column(row: &AnyRow, column: &str) -> Option { let raw_value = row .try_get_raw(column) .expect("Unable to get raw value from row"); @@ -3686,10 +3634,17 @@ pub async fn record_row_change_tx( .into()); } - fn to_text(row: Option<&SerdeMap>) -> String { + fn to_text(row: Option<&SerdeMap>, omit_redundant_fields: bool) -> String { match row { None => "NULL".to_string(), - Some(r) => format!("{}", json!(r)), + Some(row) => { + let mut row = row.clone(); + if omit_redundant_fields { + row.remove("column"); + row.remove("value"); + } + format!("{}", json!(row)) + } } } @@ -3786,7 +3741,7 @@ pub async fn record_row_change_tx( format_value(&new_value.to_string(), &numeric_re), )), ); - let column_summary = to_text(Some(&column_summary)); + let column_summary = to_text(Some(&column_summary), false); summary.push(column_summary); } } @@ -3796,7 +3751,7 @@ pub async fn record_row_change_tx( } let summary = summarize(from, to)?; - let (from, to) = (to_text(from), to_text(to)); + let (from, to) = (to_text(from, true), to_text(to, true)); let mut params = vec![table]; let from_param = { if from == "NULL" { @@ -3840,21 +3795,28 @@ pub async fn record_row_change_tx( Ok(()) } +// TODO: Remove the unwraps in this function: /// Given a database record representing either an undo or a redo from the history table, /// convert it to a vector of [ValveChange] structs. -pub fn convert_undo_or_redo_record_to_change(record: &AnyRow) -> Result> { +pub fn convert_undo_or_redo_record_to_change(record: &AnyRow) -> Result { let table: &str = record.get("table"); let row_number: i64 = record.get("row"); let row_number = row_number as u32; - let from = get_json_object_from_row(&record, "from"); - let to = get_json_object_from_row(&record, "to"); + let history_id: i32 = record.get("history_id"); + let history_id = history_id as u16; + let from = get_json_object_from_column(&record, "from"); + let to = get_json_object_from_column(&record, "to"); let summary = { let summary = record.try_get_raw("summary")?; if !summary.is_null() { let summary: &str = record.get("summary"); match serde_json::from_str::(summary) { Ok(SerdeValue::Array(v)) => Some(v), - _ => panic!("{} is not an array.", summary), + _ => { + return Err( + ValveError::InputError(format!("{summary} is not an array.")).into(), + ) + } } } else { None @@ -3866,11 +3828,26 @@ pub fn convert_undo_or_redo_record_to_change(record: &AnyRow) -> Result Result { + let mut column_changes = vec![]; + for (column, change) in &from { + match change { + SerdeValue::Number(rn) if column == "previous_row" => { + column_changes.push(ValveChange { + column: column.to_string(), + level: "delete".to_string(), + old_value: rn.to_string(), + value: "".to_string(), + message: "".to_string(), + }) + } + SerdeValue::Object(details) => { + let old_value = details + .get("value") + .and_then(|s| Some(s.to_string())) + .unwrap(); + column_changes.push(ValveChange { + column: column.to_string(), + level: "delete".to_string(), + old_value: old_value.to_string(), + value: "".to_string(), + message: "".to_string(), + }); + } + _ => panic!("Invalid change: {change}"), + }; + } + Ok(ValveRowChange { + history_id: history_id, + table: table.to_string(), + row: row_number, + message: format!("Delete row {} from '{}'", row_number, table), + changes: column_changes, + }) } - return Ok(Some(ValveRowChange { - table: table.to_string(), - row: row_number, - message: format!("Delete row {} from '{}'", row_number, table), - changes: column_changes, - })); - } - - // If `to` is not null then this is an insert (i.e., the row has changed from nothing to - // something). - if let Some(to) = to { - let mut column_changes = vec![]; - for (column, change) in to { - let value = change.get("value").and_then(|s| s.as_str()).unwrap(); - column_changes.push(ValveChange { - column: column.to_string(), - level: "insert".to_string(), - old_value: "".to_string(), - value: value.to_string(), - message: "".to_string(), - }); + (None, Some(to)) => { + let mut column_changes = vec![]; + for (column, change) in &to { + match change { + SerdeValue::Number(rn) if column == "previous_row" => { + column_changes.push(ValveChange { + column: column.to_string(), + level: "insert".to_string(), + old_value: rn.to_string(), + value: "".to_string(), + message: "".to_string(), + }) + } + SerdeValue::Object(details) => { + let value = details + .get("value") + .and_then(|s| Some(s.to_string())) + .unwrap(); + column_changes.push(ValveChange { + column: column.to_string(), + level: "insert".to_string(), + old_value: "".to_string(), + value: value.to_string(), + message: "".to_string(), + }); + } + _ => panic!("Invalid change: {change}"), + }; + } + Ok(ValveRowChange { + history_id: history_id, + table: table.to_string(), + row: row_number, + message: format!("Add row {} to '{}'", row_number, table), + changes: column_changes, + }) } - return Ok(Some(ValveRowChange { - table: table.to_string(), - row: row_number, - message: format!("Add row {} to '{}'", row_number, table), - changes: column_changes, - })); + _ => Err(ValveError::InputError("Invalid undo/redo record".to_string()).into()), } - - // If we get to here, then `summary`, `from`, and `to` are all null so we return an error. - Err(ValveError::InputError( - "All of summary, from, and to are NULL in undo/redo record".to_string(), - ) - .into()) } -/// Return the next recorded change to the data that can be redone, or None if there isn't any. -pub async fn get_record_to_redo(pool: &AnyPool) -> Result> { - // Look in the history table, get the row with the greatest ID, get the row number, - // from, and to, and determine whether the last operation was a delete, insert, or update. - let is_not_clause = if DbKind::from_pool(pool)? == DbKind::Sqlite { +/// Return an ordered list of the undone changes that can be redone. If `limit` is nonzero, return +/// no more than that many database records. +pub async fn get_db_records_to_redo(pool: &AnyPool, limit: usize) -> Result> { + let is_not = if DbKind::from_pool(pool)? == DbKind::Sqlite { "IS NOT" } else { "IS DISTINCT FROM" }; + let limit_clause = if limit > 0 { + format!(" LIMIT {limit}") + } else { + "".to_string() + }; let sql = format!( r#"SELECT * FROM "history" - WHERE "undone_by" {} NULL - ORDER BY "timestamp" DESC LIMIT 1"#, - is_not_clause + WHERE "undone_by" {is_not} NULL + ORDER BY "timestamp" DESC{limit_clause}"# ); let query = sqlx_query(&sql); - let result_row = query.fetch_optional(pool).await?; - Ok(result_row) + Ok(query.fetch_all(pool).await?) } -/// Return the next recorded change to the data that can be undone, or None if there isn't any. -pub async fn get_record_to_undo(pool: &AnyPool) -> Result> { - // Look in the history table, get the row with the greatest ID, get the row number, - // from, and to, and determine whether the last operation was a delete, insert, or update. - let is_clause = if DbKind::from_pool(pool)? == DbKind::Sqlite { +/// Return an ordered list of the changes that can be undone. If `limit` is nonzero, return no +/// more than that many database records. +pub async fn get_db_records_to_undo(pool: &AnyPool, limit: usize) -> Result> { + let is = if DbKind::from_pool(pool)? == DbKind::Sqlite { "IS" } else { "IS NOT DISTINCT FROM" }; + let limit_clause = if limit > 0 { + format!(" LIMIT {limit}") + } else { + "".to_string() + }; let sql = format!( r#"SELECT * FROM "history" - WHERE "undone_by" {} NULL - ORDER BY "history_id" DESC LIMIT 1"#, - is_clause + WHERE "undone_by" {is} NULL + ORDER BY "history_id" DESC{limit_clause}"# ); let query = sqlx_query(&sql); - let result_row = query.fetch_optional(pool).await?; - Ok(result_row) + Ok(query.fetch_all(pool).await?) } pub async fn undo_or_redo_move_tx( @@ -3987,7 +4000,7 @@ pub async fn undo_or_redo_move_tx( tx: &mut Transaction<'_, sqlx::Any>, undo: bool, ) -> Result<()> { - let summary = match get_json_array_from_row(&last_change, "summary") { + let summary = match get_json_array_from_column(&last_change, "summary") { None => { return Err(ValveError::DataError(format!( "No summary found in undo record for history_id == {}", diff --git a/src/valve.rs b/src/valve.rs index 9fc0673..9311ce8 100644 --- a/src/valve.rs +++ b/src/valve.rs @@ -8,14 +8,14 @@ use crate::{ add_message_counts, cast_column_sql_to_text, convert_undo_or_redo_record_to_change, correct_row_datatypes, delete_row_tx, generate_datatype_conditions, generate_rule_conditions, get_column_for_label, get_column_value_as_string, - get_json_array_from_row, get_json_object_from_row, get_parsed_structure_conditions, - get_pool_from_connection_string, get_previous_row_tx, get_record_to_redo, - get_record_to_undo, get_sql_for_standard_view, get_sql_for_text_view, get_sql_type, - get_sql_type_from_global_config, get_text_row_from_db_tx, insert_chunks, insert_new_row_tx, - local_sql_syntax, move_row_tx, normalize_options, read_config_files, record_row_change_tx, - record_row_move_tx, switch_undone_state_tx, undo_or_redo_move_tx, update_row_tx, - verify_table_deps_and_sort, ColumnRule, CompiledCondition, DbKind, ParsedStructure, - ValueType, + get_db_records_to_redo, get_db_records_to_undo, get_json_array_from_column, + get_json_object_from_column, get_parsed_structure_conditions, + get_pool_from_connection_string, get_previous_row_tx, get_sql_for_standard_view, + get_sql_for_text_view, get_sql_type, get_sql_type_from_global_config, + get_text_row_from_db_tx, insert_chunks, insert_new_row_tx, local_sql_syntax, move_row_tx, + normalize_options, read_config_files, record_row_change_tx, record_row_move_tx, + switch_undone_state_tx, undo_or_redo_move_tx, update_row_tx, verify_table_deps_and_sort, + ColumnRule, CompiledCondition, DbKind, ParsedStructure, ValueType, }, validate::{validate_row_tx, validate_tree_foreign_keys, with_tree_sql}, valve_grammar::StartParser, @@ -31,7 +31,10 @@ use regex::Regex; use serde::{Deserialize, Serialize}; use serde_json::{json, Value as SerdeValue}; use sprintf::sprintf; -use sqlx::{any::AnyPool, query as sqlx_query, Row, ValueRef}; +use sqlx::{ + any::{AnyPool, AnyRow}, + query as sqlx_query, Column, Row, ValueRef, +}; use std::{ collections::{HashMap, HashSet}, error::Error, @@ -121,6 +124,22 @@ impl ValveRow { }) } + /// Given a [ValveRow], convert its contents to a [JsonRow] in simple format and return it. + pub fn contents_to_simple_json(&self) -> Result { + let row_contents = serde_json::to_value(self.contents.clone())?; + let row_contents = row_contents + .as_object() + .ok_or(ValveError::InputError(format!( + "Could not convert {:?} to a rich JSON object", + row_contents + )))?; + let row_contents = row_contents + .iter() + .map(|(key, cell)| (key.clone(), cell.get("value").expect("No value").clone())) + .collect::(); + Ok(row_contents) + } + /// Given a row, with the given row number, represented as a JSON object in the following /// ('rich') format: /// ``` @@ -180,20 +199,86 @@ impl ValveRow { .cloned() } - /// Given a [ValveRow], convert its contents to a [JsonRow] in simple format and return it. - pub fn contents_to_simple_json(&self) -> Result { - let row_contents = serde_json::to_value(self.contents.clone())?; - let row_contents = row_contents - .as_object() - .ok_or(ValveError::InputError(format!( - "Could not convert {:?} to a rich JSON object", - row_contents - )))?; - let row_contents = row_contents - .iter() - .map(|(key, cell)| (key.clone(), cell.get("value").expect("No value").clone())) - .collect::(); - Ok(row_contents) + /// TODO: Add a docstring + pub fn from_any_row( + config: &ValveConfig, + kind: &DbKind, + table: &str, + any_row: &AnyRow, + global_sql_type: &Option<&str>, + ) -> Result { + let messages = { + let raw_messages = any_row.try_get_raw("message")?; + if raw_messages.is_null() { + vec![] + } else { + let messages: &str = any_row.get("message"); + match serde_json::from_str::(messages) { + Err(e) => return Err(e.into()), + Ok(SerdeValue::Array(m)) => m, + _ => { + return Err(ValveError::DataError( + format!("{} is not an array.", messages).into(), + ) + .into()) + } + } + } + }; + let row_number: i64 = any_row.get::("row_number"); + let row_number = row_number as u32; + let mut row = ValveRow { + row_number: Some(row_number), + ..Default::default() + }; + for column in any_row.columns() { + let cname = column.name(); + if !vec!["row_number", "message"].contains(&cname) { + let raw_value = any_row.try_get_raw(format!(r#"{}"#, cname).as_str())?; + let value; + if !raw_value.is_null() { + let sql_type = match global_sql_type { + Some(sql_type) => sql_type.to_string(), + None => get_sql_type_from_global_config(config, table, cname, kind), + }; + value = get_column_value_as_string(&any_row, &cname, &sql_type); + } else { + value = String::from(""); + } + let column_messages = messages + .iter() + .filter(|m| m.get("column").unwrap_or(&json!("")).as_str() == Some(cname)) + .map(|message| { + let level = message + .get("level") + .and_then(|l| l.as_str()) + .unwrap_or("No 'level' found"); + let rule = message + .get("rule") + .and_then(|l| l.as_str()) + .unwrap_or("No 'rule' found"); + let message = message + .get("message") + .and_then(|l| l.as_str()) + .unwrap_or("No 'message' found"); + ValveCellMessage { + level: level.to_string(), + rule: rule.to_string(), + message: message.to_string(), + } + }) + .collect::>(); + let valid = column_messages.iter().all(|m| m.level != "error"); + let cell = ValveCell { + value: json!(value), + valid: valid, + messages: column_messages, + ..Default::default() + }; + row.contents.insert(cname.to_string(), cell); + } + } + Ok(row) } } @@ -314,6 +399,8 @@ pub struct ValveMessage { /// Represents a change to a row in a database table. #[derive(Debug, Default, Deserialize, Serialize)] pub struct ValveRowChange { + /// An identifier for this particular change + pub history_id: u16, /// The name of the table that the change is from pub table: String, /// The row number of the changed row @@ -324,6 +411,7 @@ pub struct ValveRowChange { pub changes: Vec, } +// TODO: We should probably change the type of old_value and value to SerdeValue. /// Represents a change to a value in a row of a database table. #[derive(Debug, Default, Deserialize, Serialize)] pub struct ValveChange { @@ -2717,39 +2805,60 @@ impl Valve { self.execute_sql(&sql).await } - /// Return the next recorded change to the data that can be undone, or None if there isn't any. - pub async fn get_change_to_undo(&self) -> Result> { - match get_record_to_undo(&self.pool).await? { - None => Ok(None), - Some(record) => convert_undo_or_redo_record_to_change(&record), + /// Return an ordered list of the changes that can be undone. If `limit` is nonzero, return no + /// more than that many change records. + pub async fn get_changes_to_undo(&self, limit: usize) -> Result> { + let records = get_db_records_to_undo(&self.pool, limit).await?; + let mut changes = vec![]; + for record in &records { + match convert_undo_or_redo_record_to_change(record) { + Err(e) => return Err(e.into()), + Ok(change) => changes.push(change), + }; } - } - - /// Return the next recorded change to the data that can be redone, or None if there isn't any. - pub async fn get_change_to_redo(&self) -> Result> { - match get_record_to_redo(&self.pool).await? { - None => Ok(None), - Some(record) => convert_undo_or_redo_record_to_change(&record), + Ok(changes) + } + + /// Return an ordered list of the changes that can be undone. If `limit` is nonzero, return no + /// more than that many change records. + pub async fn get_changes_to_redo(&self, limit: usize) -> Result> { + let records = get_db_records_to_redo(&self.pool, limit).await?; + let mut changes = vec![]; + for record in &records { + match convert_undo_or_redo_record_to_change(record) { + Err(e) => return Err(e.into()), + Ok(change) => changes.push(change), + }; } + Ok(changes) } /// Undo one change and return the change record or None if there was no change to undo. pub async fn undo(&self) -> Result> { - let last_change = match get_record_to_undo(&self.pool).await? { - None => { + let records = get_db_records_to_undo(&self.pool, 1).await?; + let last_change = match records.len() { + 0 => { log::warn!("Nothing to undo."); return Ok(None); } - Some(r) => r, + 1 => &records[0], + _ => { + return Err(ValveError::DataError(format!( + "Too many records to undo: {}", + records.len() + )) + .into()) + } }; + let history_id: i32 = last_change.get("history_id"); let history_id = history_id as u16; let table: &str = last_change.get("table"); let row_number: i64 = last_change.get("row"); let row_number = row_number as u32; - let from = get_json_object_from_row(&last_change, "from"); - let to = get_json_object_from_row(&last_change, "to"); - let summary = get_json_array_from_row(&last_change, "summary"); + let from = get_json_object_from_column(&last_change, "from"); + let to = get_json_object_from_column(&last_change, "to"); + let summary = get_json_array_from_column(&last_change, "summary"); if let Some(summary) = summary { let num_moves = summary .iter() @@ -2885,12 +2994,14 @@ impl Valve { /// Redo one change and return the change record or None if there was no change to redo. pub async fn redo(&self) -> Result> { - let last_undo = match get_record_to_redo(&self.pool).await? { - None => { + let records = get_db_records_to_redo(&self.pool, 1).await?; + let last_undo = match records.len() { + 0 => { log::warn!("Nothing to redo."); return Ok(None); } - Some(last_undo) => { + 1 => { + let last_undo = &records[0]; let undone_by = last_undo.try_get_raw("undone_by")?; if undone_by.is_null() { log::warn!("Nothing to redo."); @@ -2898,15 +3009,22 @@ impl Valve { } last_undo } + _ => { + return Err(ValveError::DataError(format!( + "Too many records to redo: {}", + records.len() + )) + .into()) + } }; let history_id: i32 = last_undo.get("history_id"); let history_id = history_id as u16; let table: &str = last_undo.get("table"); let row_number: i64 = last_undo.get("row"); let row_number = row_number as u32; - let from = get_json_object_from_row(&last_undo, "from"); - let to = get_json_object_from_row(&last_undo, "to"); - let summary = get_json_array_from_row(&last_undo, "summary"); + let from = get_json_object_from_column(&last_undo, "from"); + let to = get_json_object_from_column(&last_undo, "to"); + let summary = get_json_array_from_column(&last_undo, "summary"); if let Some(summary) = summary { let num_moves = summary .iter() diff --git a/test/expected/history.tsv b/test/expected/history.tsv index bcd6d44..cf6e261 100644 --- a/test/expected/history.tsv +++ b/test/expected/history.tsv @@ -1,7 +1,7 @@ history_id table row from to summary user undone_by -1 table2 1 {"bar":{"messages":[],"valid":true,"value":""},"child":{"messages":[],"valid":true,"value":"a"},"foo":{"messages":[{"column":"foo","level":"error","message":"bar cannot be null if foo is not null","rule":"rule:foo-2","value":"5"},{"column":"foo","level":"error","message":"bar must be 'y' or 'z' if foo = 5","rule":"rule:foo-4","value":"5"}],"valid":false,"value":"5"},"parent":{"messages":[],"valid":true,"value":"b"},"xyzzy":{"messages":[],"valid":true,"value":"d"}} {"bar":{"messages":[],"valid":true,"value":"B"},"child":{"messages":[{"level":"error","message":"Values of child must be unique","rule":"key:unique"}],"valid":false,"value":"b"},"foo":{"messages":[],"valid":true,"value":1},"parent":{"messages":[],"valid":true,"value":"f"},"xyzzy":{"messages":[],"valid":true,"value":"w"}} [{"column":"bar","level":"update","message":"Value changed from '' to 'B'","old_value":"","value":"B"},{"column":"child","level":"update","message":"Value changed from 'a' to 'b'","old_value":"a","value":"b"},{"column":"foo","level":"update","message":"Value changed from 5 to 1","old_value":"5","value":"1"},{"column":"parent","level":"update","message":"Value changed from 'b' to 'f'","old_value":"b","value":"f"},{"column":"xyzzy","level":"update","message":"Value changed from 'd' to 'w'","old_value":"d","value":"w"}] VALVE +1 table2 1 {"bar":{"messages":[],"valid":true,"value":""},"child":{"messages":[],"valid":true,"value":"a"},"foo":{"messages":[{"level":"error","message":"bar cannot be null if foo is not null","rule":"rule:foo-2"},{"level":"error","message":"bar must be 'y' or 'z' if foo = 5","rule":"rule:foo-4"}],"valid":false,"value":"5"},"parent":{"messages":[],"valid":true,"value":"b"},"xyzzy":{"messages":[],"valid":true,"value":"d"}} {"bar":{"messages":[],"valid":true,"value":"B"},"child":{"messages":[{"level":"error","message":"Values of child must be unique","rule":"key:unique"}],"valid":false,"value":"b"},"foo":{"messages":[],"valid":true,"value":1},"parent":{"messages":[],"valid":true,"value":"f"},"xyzzy":{"messages":[],"valid":true,"value":"w"}} [{"column":"bar","level":"update","message":"Value changed from '' to 'B'","old_value":"","value":"B"},{"column":"child","level":"update","message":"Value changed from 'a' to 'b'","old_value":"a","value":"b"},{"column":"foo","level":"update","message":"Value changed from 5 to 1","old_value":"5","value":"1"},{"column":"parent","level":"update","message":"Value changed from 'b' to 'f'","old_value":"b","value":"f"},{"column":"xyzzy","level":"update","message":"Value changed from 'd' to 'w'","old_value":"d","value":"w"}] VALVE 2 table3 11 {"id":{"messages":[],"valid":true,"value":"BFO:0000027"},"label":{"messages":[],"valid":true,"value":"bazaar"},"parent":{"messages":[{"level":"error","message":"Value 'barrie' of column parent is not in column label","rule":"tree:foreign"}],"valid":false,"value":"barrie"},"source":{"messages":[{"level":"error","message":"Value 'BFOBBER' of column source is not in table1.prefix","rule":"key:foreign"}],"valid":false,"value":"BFOBBER"},"type":{"messages":[],"valid":true,"value":"owl:Class"}} VALVE -3 table6 1 {"bar":{"messages":[],"valid":true,"value":""},"child":{"messages":[],"valid":true,"value":"1"},"foo":{"messages":[{"column":"foo","level":"error","message":"bar cannot be null if foo is not null","rule":"rule:foo-2","value":"e"},{"column":"foo","level":"error","message":"bar must be 25 or 26 if foo = 'e'","rule":"rule:foo-4","value":"e"}],"valid":false,"value":"e"},"parent":{"messages":[],"valid":true,"value":"2"},"xyzzy":{"messages":[],"valid":true,"value":"4"}} {"bar":{"messages":[],"valid":true,"value":2},"child":{"messages":[{"level":"error","message":"Values of child must be unique","rule":"key:unique"}],"valid":false,"value":2},"foo":{"messages":[],"valid":true,"value":"a"},"parent":{"messages":[],"valid":true,"value":6},"xyzzy":{"messages":[],"valid":true,"value":23}} [{"column":"bar","level":"update","message":"Value changed from '' to 2","old_value":"","value":"2"},{"column":"child","level":"update","message":"Value changed from 1 to 2","old_value":"1","value":"2"},{"column":"foo","level":"update","message":"Value changed from 'e' to 'a'","old_value":"e","value":"a"},{"column":"parent","level":"update","message":"Value changed from 2 to 6","old_value":"2","value":"6"},{"column":"xyzzy","level":"update","message":"Value changed from 4 to 23","old_value":"4","value":"23"}] VALVE +3 table6 1 {"bar":{"messages":[],"valid":true,"value":""},"child":{"messages":[],"valid":true,"value":"1"},"foo":{"messages":[{"level":"error","message":"bar cannot be null if foo is not null","rule":"rule:foo-2"},{"level":"error","message":"bar must be 25 or 26 if foo = 'e'","rule":"rule:foo-4"}],"valid":false,"value":"e"},"parent":{"messages":[],"valid":true,"value":"2"},"xyzzy":{"messages":[],"valid":true,"value":"4"}} {"bar":{"messages":[],"valid":true,"value":2},"child":{"messages":[{"level":"error","message":"Values of child must be unique","rule":"key:unique"}],"valid":false,"value":2},"foo":{"messages":[],"valid":true,"value":"a"},"parent":{"messages":[],"valid":true,"value":6},"xyzzy":{"messages":[],"valid":true,"value":23}} [{"column":"bar","level":"update","message":"Value changed from '' to 2","old_value":"","value":"2"},{"column":"child","level":"update","message":"Value changed from 1 to 2","old_value":"1","value":"2"},{"column":"foo","level":"update","message":"Value changed from 'e' to 'a'","old_value":"e","value":"a"},{"column":"parent","level":"update","message":"Value changed from 2 to 6","old_value":"2","value":"6"},{"column":"xyzzy","level":"update","message":"Value changed from 4 to 23","old_value":"4","value":"23"}] VALVE 4 table6 10 {"bar":{"messages":[],"valid":true,"value":2},"child":{"messages":[{"level":"error","message":"Values of child must be unique","rule":"key:unique"}],"valid":false,"value":2},"foo":{"messages":[],"valid":true,"value":"a"},"parent":{"messages":[],"valid":true,"value":6},"xyzzy":{"messages":[],"valid":true,"value":23}} VALVE 5 table3 12 {"id":{"messages":[],"valid":true,"value":"BFO:0000099"},"label":{"messages":[],"valid":true,"value":"jafar"},"parent":{"messages":[],"valid":true,"value":"mar"},"source":{"messages":[],"valid":true,"value":"COB"},"type":{"messages":[],"valid":true,"value":"owl:Class"}} VALVE 6 table10 1 {"foreign_column":{"messages":[],"valid":true,"value":"a"},"numeric_foreign_column":{"messages":[],"valid":true,"value":"1"},"other_foreign_column":{"messages":[],"valid":true,"value":"a"}} {"foreign_column":{"messages":[],"valid":true,"value":"w"},"numeric_foreign_column":{"messages":[{"level":"error","message":"numeric_foreign_column should be a positive or negative integer","rule":"datatype:integer"},{"level":"error","message":"numeric_foreign_column should be a line of text that does not begin or end with whitespace","rule":"datatype:trimmed_line"}],"valid":false,"value":""},"other_foreign_column":{"messages":[],"valid":true,"value":"z"}} [{"column":"foreign_column","level":"update","message":"Value changed from 'a' to 'w'","old_value":"a","value":"w"},{"column":"numeric_foreign_column","level":"update","message":"Value changed from 1 to ''","old_value":"1","value":""},{"column":"other_foreign_column","level":"update","message":"Value changed from 'a' to 'z'","old_value":"a","value":"z"}] VALVE