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}