diff --git a/src/api_test.rs b/src/api_test.rs index 40904bcb..11f0f3ea 100644 --- a/src/api_test.rs +++ b/src/api_test.rs @@ -1,6 +1,7 @@ use ontodev_valve::{ delete_row, get_compiled_datatype_conditions, get_compiled_rule_conditions, - get_parsed_structure_conditions, insert_new_row, redo, undo, update_row, + get_parsed_structure_conditions, get_record_to_redo, get_record_to_undo, insert_new_row, redo, + undo, update_row, validate::{get_matching_values, validate_row}, valve, valve_grammar::StartParser, @@ -620,6 +621,36 @@ async fn test_randomized_api_test_with_undo_redo( Ok(()) } +async fn verify_undo_redo( + pool: &AnyPool, + undo_should_exist: bool, + redo_should_exist: bool, +) -> Result<(), sqlx::Error> { + let rec_to_undo = get_record_to_undo(pool).await?; + if undo_should_exist { + if let None = rec_to_undo { + assert!(false, "Expected a record to undo."); + } + } else { + if let Some(_) = rec_to_undo { + assert!(false, "Did not expect a record to undo."); + } + } + + let rec_to_redo = get_record_to_redo(pool).await?; + if redo_should_exist { + if let None = rec_to_redo { + assert!(false, "Expected a record to redo."); + } + } else { + if let Some(_) = rec_to_redo { + assert!(false, "Did not expect a record to redo."); + } + } + + Ok(()) +} + async fn test_undo_redo( config: &SerdeMap, compiled_datatype_conditions: &HashMap, @@ -639,6 +670,9 @@ async fn test_undo_redo( "numeric_foreign_column": {"messages": [], "valid": true, "value": "11"}, }); + // Our initial undo/redo state: + verify_undo_redo(pool, false, false).await?; + // Undo/redo test 1: let _rn = insert_new_row( &config, @@ -652,6 +686,8 @@ async fn test_undo_redo( ) .await?; + verify_undo_redo(pool, true, false).await?; + undo( &config, &compiled_datatype_conditions, @@ -661,6 +697,8 @@ async fn test_undo_redo( ) .await?; + verify_undo_redo(pool, false, true).await?; + redo( &config, &compiled_datatype_conditions, @@ -670,6 +708,8 @@ async fn test_undo_redo( ) .await?; + verify_undo_redo(pool, true, false).await?; + undo( &config, &compiled_datatype_conditions, @@ -679,6 +719,8 @@ async fn test_undo_redo( ) .await?; + verify_undo_redo(pool, false, true).await?; + // Undo/redo test 2: update_row( &config, @@ -692,6 +734,8 @@ async fn test_undo_redo( ) .await?; + verify_undo_redo(pool, true, false).await?; + undo( &config, &compiled_datatype_conditions, @@ -701,6 +745,8 @@ async fn test_undo_redo( ) .await?; + verify_undo_redo(pool, false, true).await?; + redo( &config, &compiled_datatype_conditions, @@ -710,6 +756,8 @@ async fn test_undo_redo( ) .await?; + verify_undo_redo(pool, true, false).await?; + undo( &config, &compiled_datatype_conditions, @@ -719,6 +767,8 @@ async fn test_undo_redo( ) .await?; + verify_undo_redo(pool, false, true).await?; + // Undo/redo test 3: delete_row( &config, @@ -731,6 +781,8 @@ async fn test_undo_redo( ) .await?; + verify_undo_redo(pool, true, false).await?; + undo( &config, &compiled_datatype_conditions, @@ -740,6 +792,8 @@ async fn test_undo_redo( ) .await?; + verify_undo_redo(pool, false, true).await?; + redo( &config, &compiled_datatype_conditions, @@ -749,6 +803,8 @@ async fn test_undo_redo( ) .await?; + verify_undo_redo(pool, true, false).await?; + undo( &config, &compiled_datatype_conditions, @@ -758,6 +814,8 @@ async fn test_undo_redo( ) .await?; + verify_undo_redo(pool, false, true).await?; + // Undo/redo test 4: let rn = insert_new_row( &config, @@ -771,6 +829,8 @@ async fn test_undo_redo( ) .await?; + verify_undo_redo(pool, true, false).await?; + update_row( &config, &compiled_datatype_conditions, @@ -783,6 +843,8 @@ async fn test_undo_redo( ) .await?; + verify_undo_redo(pool, true, false).await?; + // Undo update: undo( &config, @@ -793,6 +855,8 @@ async fn test_undo_redo( ) .await?; + verify_undo_redo(pool, true, true).await?; + // Redo update: redo( &config, @@ -803,6 +867,8 @@ async fn test_undo_redo( ) .await?; + verify_undo_redo(pool, true, false).await?; + delete_row( &config, &compiled_datatype_conditions, @@ -814,6 +880,8 @@ async fn test_undo_redo( ) .await?; + verify_undo_redo(pool, true, false).await?; + // Undo delete: undo( &config, @@ -824,6 +892,8 @@ async fn test_undo_redo( ) .await?; + verify_undo_redo(pool, true, true).await?; + // Undo update: undo( &config, @@ -834,6 +904,8 @@ async fn test_undo_redo( ) .await?; + verify_undo_redo(pool, true, true).await?; + // Undo insert: undo( &config, @@ -844,6 +916,8 @@ async fn test_undo_redo( ) .await?; + verify_undo_redo(pool, false, true).await?; + eprintln!("done."); Ok(()) } @@ -901,6 +975,13 @@ pub async fn run_api_tests(table: &str, database: &str) -> Result<(), sqlx::Erro // NOTE that you must use an external script to fetch the data from the database and run a diff // against a known good sample to verify that these tests yield the expected results: + test_undo_redo( + &config, + &compiled_datatype_conditions, + &compiled_rule_conditions, + &pool, + ) + .await?; test_matching( &config, &compiled_datatype_conditions, @@ -943,14 +1024,6 @@ pub async fn run_api_tests(table: &str, database: &str) -> Result<(), sqlx::Erro &pool, ) .await?; - test_undo_redo( - &config, - &compiled_datatype_conditions, - &compiled_rule_conditions, - &pool, - ) - .await?; - test_randomized_api_test_with_undo_redo( &config, &compiled_datatype_conditions, diff --git a/src/lib.rs b/src/lib.rs index 296fd7cf..6979a678 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2090,11 +2090,22 @@ pub async fn get_record_to_redo(pool: &AnyPool) -> Result, sqlx:: } else { "IS DISTINCT FROM" }; + let is_clause = if pool.any_kind() == AnyKind::Sqlite { + "IS" + } else { + "IS NOT DISTINCT FROM" + }; let sql = format!( - r#"SELECT * FROM "history" - WHERE "undone_by" {} NULL + r#"SELECT * FROM "history" h1 + WHERE "undone_by" {is_not} NULL + AND NOT EXISTS ( + SELECT 1 FROM "history" h2 + WHERE h2.history_id > h1.history_id + AND "undone_by" {is} NULL + ) ORDER BY "timestamp" DESC LIMIT 1"#, - is_not_clause + is_not = is_not_clause, + is = is_clause ); let query = sqlx_query(&sql); let result_row = query.fetch_optional(pool).await?; @@ -3278,7 +3289,7 @@ fn cast_column_sql_to_text(column: &str, sql_type: &str) -> String { /// Given a database row, the name of a column, and it's SQL type, return the value of that column /// from the given row as a String. -fn get_column_value(row: &AnyRow, column: &str, sql_type: &str) -> String { +pub fn get_column_value(row: &AnyRow, column: &str, sql_type: &str) -> String { let s = sql_type.to_lowercase(); if s == "numeric" { let value: f64 = row.get(format!(r#"{}"#, column).as_str()); diff --git a/test/expected/history.tsv b/test/expected/history.tsv index 23f15d77..86afe795 100644 --- a/test/expected/history.tsv +++ b/test/expected/history.tsv @@ -1,15 +1,15 @@ 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":[{"level":"error","message":"An unrelated error","rule":"custom:unrelated"}],"valid":false,"value":"B"},"child":{"messages":[{"level":"error","message":"Values of child must be unique","rule":"tree:child-unique"}],"valid":false,"value":"b"},"foo":{"messages":[],"valid":true,"value":"1"},"parent":{"messages":[],"valid":true,"value":"f"},"xyzzy":{"messages":[{"level":"error","message":"Value 'w' of column xyzzy is not in table2.child","rule":"under:not-in-tree"}],"valid":false,"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":"An unrelated error","rule":"custom:unrelated"},{"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":[{"level":"error","message":"An unrelated error","rule":"custom:unrelated"}],"valid":false,"value":"2"},"child":{"messages":[{"level":"error","message":"Values of child must be unique","rule":"tree:child-unique"}],"valid":false,"value":"2"},"foo":{"messages":[],"valid":true,"value":"a"},"parent":{"messages":[],"valid":true,"value":"6"},"xyzzy":{"messages":[{"level":"error","message":"Value '23' of column xyzzy is not in table6.child","rule":"under:not-in-tree"}],"valid":false,"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":[{"level":"error","message":"An unrelated error","rule":"custom:unrelated"}],"valid":false,"value":"2"},"child":{"messages":[{"level":"error","message":"Values of child must be unique","rule":"tree:child-unique"}],"valid":false,"value":"2"},"foo":{"messages":[],"valid":true,"value":"a"},"parent":{"messages":[],"valid":true,"value":"6"},"xyzzy":{"messages":[{"level":"error","message":"Value '23' of column xyzzy is not in table6.child","rule":"under:not-in-tree"}],"valid":false,"value":"23"}} VALVE -5 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 -6 table11 2 {"bar":{"messages":[],"valid":true,"value":"f"},"child":{"messages":[],"valid":true,"value":"b"},"foo":{"messages":[],"valid":true,"value":"e"},"parent":{"messages":[],"valid":true,"value":"c"},"xyzzy":{"messages":[],"valid":true,"value":"d"}} {"bar":{"messages":[],"valid":true,"value":"f"},"child":{"messages":[],"valid":true,"value":"b"},"foo":{"messages":[{"level":"error","message":"Values of foo must be unique","rule":"key:primary"}],"valid":false,"value":"d"},"parent":{"messages":[],"valid":true,"value":"c"},"xyzzy":{"messages":[],"valid":true,"value":"d"}} [{"column":"foo","level":"update","message":"Value changed from 'e' to 'd'","old_value":"e","value":"d"}] VALVE -7 table11 4 {"bar":{"messages":[],"valid":true,"value":"z"},"child":{"messages":[],"valid":true,"value":"f"},"foo":{"messages":[],"valid":true,"value":"e"},"parent":{"messages":[],"valid":true,"value":"g"},"xyzzy":{"messages":[],"valid":true,"value":"x"}} VALVE -8 table10 9 {"foreign_column":{"messages":[],"valid":true,"value":"i"},"numeric_foreign_column":{"messages":[],"valid":true,"value":"9"},"other_foreign_column":{"messages":[],"valid":true,"value":"i"}} VALVE -9 table10 10 {"foreign_column":{"messages":[],"valid":true,"value":"j"},"numeric_foreign_column":{"messages":[],"valid":true,"value":"10"},"other_foreign_column":{"messages":[],"valid":true,"value":"j"}} VALVE VALVE -10 table10 8 {"foreign_column":{"messages":[],"valid":true,"value":"h"},"numeric_foreign_column":{"messages":[],"valid":true,"value":"8"},"other_foreign_column":{"messages":[],"valid":true,"value":"h"}} {"foreign_column":{"messages":[],"valid":true,"value":"k"},"numeric_foreign_column":{"messages":[],"valid":true,"value":"11"},"other_foreign_column":{"messages":[],"valid":true,"value":"k"}} [{"column":"foreign_column","level":"update","message":"Value changed from 'h' to 'k'","old_value":"h","value":"k"},{"column":"numeric_foreign_column","level":"update","message":"Value changed from 8 to 11","old_value":"8","value":"11"},{"column":"other_foreign_column","level":"update","message":"Value changed from 'h' to 'k'","old_value":"h","value":"k"}] VALVE VALVE -11 table10 8 {"foreign_column":{"messages":[],"valid":true,"value":"h"},"numeric_foreign_column":{"messages":[],"valid":true,"value":"8"},"other_foreign_column":{"messages":[],"valid":true,"value":"h"}} VALVE VALVE -12 table10 11 {"foreign_column":{"messages":[],"valid":true,"value":"j"},"numeric_foreign_column":{"messages":[],"valid":true,"value":"10"},"other_foreign_column":{"messages":[],"valid":true,"value":"j"}} VALVE VALVE -13 table10 11 {"foreign_column":{"messages":[],"valid":true,"value":"j"},"numeric_foreign_column":{"messages":[],"valid":true,"value":"10"},"other_foreign_column":{"messages":[],"valid":true,"value":"j"}} {"foreign_column":{"messages":[],"valid":true,"value":"k"},"numeric_foreign_column":{"messages":[],"valid":true,"value":"11"},"other_foreign_column":{"messages":[],"valid":true,"value":"k"}} [{"column":"foreign_column","level":"update","message":"Value changed from 'j' to 'k'","old_value":"j","value":"k"},{"column":"numeric_foreign_column","level":"update","message":"Value changed from 10 to 11","old_value":"10","value":"11"},{"column":"other_foreign_column","level":"update","message":"Value changed from 'j' to 'k'","old_value":"j","value":"k"}] VALVE VALVE -14 table10 11 {"foreign_column":{"messages":[],"valid":true,"value":"k"},"numeric_foreign_column":{"messages":[],"valid":true,"value":"11"},"other_foreign_column":{"messages":[],"valid":true,"value":"k"}} VALVE VALVE +1 table10 9 {"foreign_column":{"messages":[],"valid":true,"value":"j"},"numeric_foreign_column":{"messages":[],"valid":true,"value":"10"},"other_foreign_column":{"messages":[],"valid":true,"value":"j"}} VALVE VALVE +2 table10 8 {"foreign_column":{"messages":[],"valid":true,"value":"h"},"numeric_foreign_column":{"messages":[],"valid":true,"value":"8"},"other_foreign_column":{"messages":[],"valid":true,"value":"h"}} {"foreign_column":{"messages":[],"valid":true,"value":"k"},"numeric_foreign_column":{"messages":[],"valid":true,"value":"11"},"other_foreign_column":{"messages":[],"valid":true,"value":"k"}} [{"column":"foreign_column","level":"update","message":"Value changed from 'h' to 'k'","old_value":"h","value":"k"},{"column":"numeric_foreign_column","level":"update","message":"Value changed from 8 to 11","old_value":"8","value":"11"},{"column":"other_foreign_column","level":"update","message":"Value changed from 'h' to 'k'","old_value":"h","value":"k"}] VALVE VALVE +3 table10 8 {"foreign_column":{"messages":[],"valid":true,"value":"h"},"numeric_foreign_column":{"messages":[],"valid":true,"value":"8"},"other_foreign_column":{"messages":[],"valid":true,"value":"h"}} VALVE VALVE +4 table10 10 {"foreign_column":{"messages":[],"valid":true,"value":"j"},"numeric_foreign_column":{"messages":[],"valid":true,"value":"10"},"other_foreign_column":{"messages":[],"valid":true,"value":"j"}} VALVE VALVE +5 table10 10 {"foreign_column":{"messages":[],"valid":true,"value":"j"},"numeric_foreign_column":{"messages":[],"valid":true,"value":"10"},"other_foreign_column":{"messages":[],"valid":true,"value":"j"}} {"foreign_column":{"messages":[],"valid":true,"value":"k"},"numeric_foreign_column":{"messages":[],"valid":true,"value":"11"},"other_foreign_column":{"messages":[],"valid":true,"value":"k"}} [{"column":"foreign_column","level":"update","message":"Value changed from 'j' to 'k'","old_value":"j","value":"k"},{"column":"numeric_foreign_column","level":"update","message":"Value changed from 10 to 11","old_value":"10","value":"11"},{"column":"other_foreign_column","level":"update","message":"Value changed from 'j' to 'k'","old_value":"j","value":"k"}] VALVE VALVE +6 table10 10 {"foreign_column":{"messages":[],"valid":true,"value":"k"},"numeric_foreign_column":{"messages":[],"valid":true,"value":"11"},"other_foreign_column":{"messages":[],"valid":true,"value":"k"}} VALVE VALVE +7 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":[{"level":"error","message":"An unrelated error","rule":"custom:unrelated"}],"valid":false,"value":"B"},"child":{"messages":[{"level":"error","message":"Values of child must be unique","rule":"tree:child-unique"}],"valid":false,"value":"b"},"foo":{"messages":[],"valid":true,"value":"1"},"parent":{"messages":[],"valid":true,"value":"f"},"xyzzy":{"messages":[{"level":"error","message":"Value 'w' of column xyzzy is not in table2.child","rule":"under:not-in-tree"}],"valid":false,"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 +8 table3 11 {"id":{"messages":[],"valid":true,"value":"BFO:0000027"},"label":{"messages":[],"valid":true,"value":"bazaar"},"parent":{"messages":[{"level":"error","message":"An unrelated error","rule":"custom:unrelated"},{"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 +9 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":[{"level":"error","message":"An unrelated error","rule":"custom:unrelated"}],"valid":false,"value":"2"},"child":{"messages":[{"level":"error","message":"Values of child must be unique","rule":"tree:child-unique"}],"valid":false,"value":"2"},"foo":{"messages":[],"valid":true,"value":"a"},"parent":{"messages":[],"valid":true,"value":"6"},"xyzzy":{"messages":[{"level":"error","message":"Value '23' of column xyzzy is not in table6.child","rule":"under:not-in-tree"}],"valid":false,"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 +10 table6 10 {"bar":{"messages":[{"level":"error","message":"An unrelated error","rule":"custom:unrelated"}],"valid":false,"value":"2"},"child":{"messages":[{"level":"error","message":"Values of child must be unique","rule":"tree:child-unique"}],"valid":false,"value":"2"},"foo":{"messages":[],"valid":true,"value":"a"},"parent":{"messages":[],"valid":true,"value":"6"},"xyzzy":{"messages":[{"level":"error","message":"Value '23' of column xyzzy is not in table6.child","rule":"under:not-in-tree"}],"valid":false,"value":"23"}} VALVE +11 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 +12 table11 2 {"bar":{"messages":[],"valid":true,"value":"f"},"child":{"messages":[],"valid":true,"value":"b"},"foo":{"messages":[],"valid":true,"value":"e"},"parent":{"messages":[],"valid":true,"value":"c"},"xyzzy":{"messages":[],"valid":true,"value":"d"}} {"bar":{"messages":[],"valid":true,"value":"f"},"child":{"messages":[],"valid":true,"value":"b"},"foo":{"messages":[{"level":"error","message":"Values of foo must be unique","rule":"key:primary"}],"valid":false,"value":"d"},"parent":{"messages":[],"valid":true,"value":"c"},"xyzzy":{"messages":[],"valid":true,"value":"d"}} [{"column":"foo","level":"update","message":"Value changed from 'e' to 'd'","old_value":"e","value":"d"}] VALVE +13 table11 4 {"bar":{"messages":[],"valid":true,"value":"z"},"child":{"messages":[],"valid":true,"value":"f"},"foo":{"messages":[],"valid":true,"value":"e"},"parent":{"messages":[],"valid":true,"value":"g"},"xyzzy":{"messages":[],"valid":true,"value":"x"}} VALVE +14 table10 11 {"foreign_column":{"messages":[],"valid":true,"value":"i"},"numeric_foreign_column":{"messages":[],"valid":true,"value":"9"},"other_foreign_column":{"messages":[],"valid":true,"value":"i"}} VALVE