A database at the previous schema version, with data in it, is brought to this one and loses nothing; and what the new version adds works on it.
4use std::path::PathBuf;
6use rusqlite::Connection; 7use whiskers_core::{ChatState, ChatStore, Entry, Event, Log, Memory, NewFact}; 8use whiskers_store::{SCHEMA_VERSION, SqliteChat, SqliteLog, SqliteMemory, SqliteReflections, Store}; 9 10const MIGRATIONS_UP_TO_5: [&str; 5] = [ 11 include_str!("../migrations/0001_init.sql"), 12 include_str!("../migrations/0002_forgetting_empties_the_log.sql"), 13 include_str!("../migrations/0003_soft_actions.sql"), 14 include_str!("../migrations/0004_speech_cache.sql"), 15 include_str!("../migrations/0005_household_characters.sql"), 16]; 17 18fn dir(name: &str) -> PathBuf { 19 let d = std::env::temp_dir().join(format!("whiskers-store-migrate-{name}-{}", std::process::id())); 20 let _ = std::fs::remove_dir_all(&d); 21 std::fs::create_dir_all(&d).unwrap(); 22 d 23}
A database exactly as version 5 of the schema left it, with a memory, a mention, a forgotten memory, a log of both sorts of line, a chat, a household and a picture.
27fn version_5(path: &PathBuf) { 28 let conn = Connection::open(path).unwrap(); 29 conn.pragma_update(None, "foreign_keys", "ON").unwrap(); 30 for (i, sql) in MIGRATIONS_UP_TO_5.iter().enumerate() { 31 conn.execute_batch(sql).unwrap(); 32 conn.execute_batch(&format!("PRAGMA user_version = {}", i + 1)).unwrap(); 33 } 34 conn.execute_batch( 35 "INSERT INTO fact (id, gid, text, learned_at_ms, kind) VALUES (1, 'g-kept', 'Biscuit is her bunny', 10, 'toy'), (2, 'g-old', 'she likes tea', 11, 'other'); 36 INSERT INTO fact_mention (gid, at_ms, device) VALUES ('g-kept', 10, 'phone'), ('g-kept', 50, 'phone'), ('g-old', 11, 'phone'); 37 INSERT INTO tombstone (gid, at_ms, device) VALUES ('g-gone', 7, 'phone'); 38 INSERT INTO picture (id, bytes) VALUES ('0000000000000001-0000.jpg', x'ffd8'); 39 INSERT INTO journal_line (device, seq, entry) VALUES 40 ('phone', 0, '{\"at_ms\":100,\"event\":{\"Heard\":{\"text\":\"hello there\",\"pictures\":[\"0000000000000001-0000.jpg\"]}}}'), 41 ('phone', 1, '{\"at_ms\":101,\"event\":{\"Said\":{\"text\":\"hi!\",\"outcome\":\"Answered\"}}}'), 42 ('phone', 2, '{\"at_ms\":102,\"event\":{\"Remembered\":{\"fact\":\"she likes tea\",\"gid\":\"g-old\"}}}'); 43 INSERT INTO journal_picture (device, seq, picture_id) VALUES ('phone', 0, '0000000000000001-0000.jpg'); 44 INSERT INTO chat_session (id, summary, last_active_ms, version) VALUES (1, 'they met', 101, 4); 45 INSERT INTO chat_turn (speaker, text) VALUES ('child', 'hello there'), ('whiskers', 'hi!'); 46 INSERT INTO household (name, value, stamp) VALUES ('daily_minutes', '30', 5); 47 INSERT INTO time_spent (device, day, used_ms) VALUES ('phone', 20000, 60000);", 48 ) 49 .unwrap(); 50}
52#[test] 53fn a_database_at_version_5_is_brought_to_this_one_with_everything_in_it() { 54 let d = dir("v5"); 55 let db = d.join("w.db"); 56 version_5(&db); 57 let store = Store::open(&db).unwrap(); 58 let conn = Connection::open(&db).unwrap(); 59 let one = |sql: &str| conn.query_row(sql, [], |r| r.get::<_, i64>(0)).unwrap(); 60 assert_eq!(one("PRAGMA user_version"), i64::from(SCHEMA_VERSION)); 61 62 // The memory, with the mentions it had (which name no turn: they were made before turns had identities). 63 let mut memory = SqliteMemory::open(store.clone(), "phone").unwrap(); 64 let kept = memory.facts().into_iter().find(|f| f.gid == "g-kept").unwrap(); 65 assert_eq!((kept.text.as_str(), kept.mentions.len()), ("Biscuit is her bunny", 2)); 66 assert!(kept.mentions.iter().all(|m| m.turn.is_empty())); 67 assert_eq!(memory.snapshot().forgotten, ["g-gone"], "and what was forgotten still is"); 68 // The log, read through the index by time that the version added. 69 assert_eq!(one("SELECT COUNT(*) FROM journal_line WHERE at_ms BETWEEN 100 AND 102"), 3, "every line has its time"); 70 let log = SqliteLog::new(store.clone(), "phone"); 71 let (groups, unreadable) = log.read_range(101, 102).unwrap(); 72 assert_eq!((groups[0].len(), unreadable), (1, 0)); 73 assert_eq!(log.pictures_shown().unwrap(), ["0000000000000001-0000.jpg"]); 74 assert_eq!(log.own_count().unwrap(), 3, "sync's counts did not move"); 75 // The chat is as it was, emptied for nothing. 76 let chat = SqliteChat::new(store.clone()).load().unwrap(); 77 assert_eq!((chat.summary.as_str(), chat.turns.len(), chat.version, chat.scrubbed.len()), ("they met", 2, 4, 0)); 78 assert_eq!(one("SELECT COUNT(*) FROM household") + one("SELECT used_ms FROM time_spent"), 1 + 60000); 79 assert_eq!(one("SELECT COUNT(*) FROM pending_reflection"), 0); 80 81 // What the version adds works on what was already there: forgetting an old memory still empties its words in the log 82 // (by identity, as before), it has no turns to empty, and the old conversation lines stay. 83 let g_old = memory.facts().into_iter().find(|f| f.gid == "g-old").unwrap(); 84 memory.forget(g_old.id, 500).unwrap(); 85 let kept_lines = log.own_entries().unwrap(); 86 assert!(matches!(&kept_lines[2].event, Event::Remembered { fact, .. } if fact.is_empty())); 87 assert!(matches!(&kept_lines[0].event, Event::Heard { text, .. } if text == "hello there"), "a turn from before turns had identities cannot be found, and is left"); 88 // A new turn is found. 89 let mut log = log; 90 log.append(&Entry::in_turn("t-new", 600, Event::Heard { text: "SECRET-4711".into(), pictures: vec![] })).unwrap(); 91 let f = memory.add(NewFact { text: "x".into(), turn: "t-new".into(), ..NewFact::default() }, 600).unwrap(); 92 memory.forget(f.id, 700).unwrap(); 93 assert!(matches!(&log.own_entries().unwrap()[3].event, Event::Heard { text, .. } if text.is_empty())); 94 let _ = SqliteReflections::new(store); 95} 96 97#[test] 98fn migrating_twice_is_a_no_op_and_a_database_from_a_newer_build_is_still_refused() { 99 let d = dir("twice"); 100 let db = d.join("w.db"); 101 version_5(&db); 102 drop(Store::open(&db).unwrap()); 103 drop(Store::open(&db).unwrap()); 104 let before = Connection::open(&db).unwrap().query_row("SELECT COUNT(*) FROM journal_line", [], |r| r.get::<_, i64>(0)).unwrap(); 105 assert_eq!(before, 3); 106 let conn = Connection::open(&db).unwrap(); 107 conn.execute_batch(&format!("PRAGMA user_version = {}", SCHEMA_VERSION + 1)).unwrap(); 108 drop(conn); 109 assert!(Store::open(&db).is_err()); 110 let _ = ChatState::default(); 111}