perf(chat): ingest a message batch in one transaction
Ingesting a scan's worth of chat messages persisted each new message with its own append() call, so a batch of fresh messages opened and committed one SQLite transaction per row. - A new appendBatch persists many messages in a single transaction, using the same INSERT OR IGNORE dedup key as append() and preserving input order so load()'s rowid ordering is unchanged. - Every message is serialized and encrypted before the transaction opens, so a message with a non-UTF-8 body is skipped on its own without leaving a transaction open; the valid siblings still persist. - Ingest collects the newly stored messages and writes them once after the loop instead of per message. It is inert (returns 0) while locked or on empty input. Tests cover a batch insert, dedup on re-insert, empty input, bad-UTF-8 isolation with a valid sibling and a following append(), insert order, and the locked no-op. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14274,6 +14274,34 @@ void testHushChatDatabase()
|
||||
EXPECT_TRUE(loaded[0].delivery == ChatDelivery::Failed);
|
||||
}
|
||||
|
||||
// appendBatch: one transaction, dedup, order, tombstone-skip, bad-UTF-8 isolation.
|
||||
scrub(dbPath);
|
||||
{
|
||||
ChatDatabase db(dbPath);
|
||||
EXPECT_TRUE(db.unlockWithSecret(seedA));
|
||||
std::vector<ChatMessage> batch = {
|
||||
makeMsg("b1", 0, "c", "one", ChatMessageKind::Message, 1),
|
||||
makeMsg("b2", 0, "c", "two", ChatMessageKind::Message, 2),
|
||||
makeMsg("b3", 0, "c", "three", ChatMessageKind::Message, 3),
|
||||
};
|
||||
EXPECT_EQ(db.appendBatch(batch), 3);
|
||||
EXPECT_EQ(db.appendBatch(batch), 0); // dedup: all ignored
|
||||
EXPECT_EQ((int)db.appendBatch({}), 0); // empty → no-op
|
||||
// bad UTF-8 body is skipped, the valid sibling still persists, and append() still works after.
|
||||
ChatMessage bad = makeMsg("b4", 0, "c", std::string("\xff\xfe", 2), ChatMessageKind::Message, 4);
|
||||
ChatMessage good = makeMsg("b5", 0, "c", "five", ChatMessageKind::Message, 5);
|
||||
EXPECT_EQ(db.appendBatch({bad, good}), 1); // only good persisted, no open txn left
|
||||
EXPECT_TRUE(db.append(makeMsg("b6", 0, "c", "six", ChatMessageKind::Message, 6)));
|
||||
std::vector<ChatMessage> all = db.load();
|
||||
EXPECT_EQ((int)all.size(), 5); // b1,b2,b3,b5,b6
|
||||
EXPECT_EQ(all[0].body, std::string("one")); // rowid/insert order
|
||||
EXPECT_EQ(all[4].body, std::string("six"));
|
||||
}
|
||||
{
|
||||
ChatDatabase locked(dbPath); // no unlock → inert
|
||||
EXPECT_EQ(locked.appendBatch({makeMsg("x", 0, "c", "x", ChatMessageKind::Message, 0)}), 0);
|
||||
}
|
||||
|
||||
scrub(dbPath); scrub(dbPath2);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user