From 899762cddd7928f808ec97bee15e89094dab436d Mon Sep 17 00:00:00 2001 From: Reto Brunner Date: Wed, 28 Dec 2022 14:57:36 +0100 Subject: [PATCH] sqlite: Add infrastructure for migration tests This sets up the testing infrastructure to test migrations we are doing. It's done on a in memory database directly, we are only interested in the statements themselves and it's easier than to try and inject a prepared db into the store. We do add some dummy data though to make sure we actually execute the things as we expect. --- server/plugins/messageStorage/sqlite.ts | 13 +++- test/plugins/sqlite.ts | 97 ++++++++++++++++++++++++- 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/server/plugins/messageStorage/sqlite.ts b/server/plugins/messageStorage/sqlite.ts index 9f3766fd..3bd67baa 100644 --- a/server/plugins/messageStorage/sqlite.ts +++ b/server/plugins/messageStorage/sqlite.ts @@ -23,16 +23,21 @@ try { ); } -export const currentSchemaVersion = 1520239200; +type Migration = {version: number; stmts: string[]}; +export const currentSchemaVersion = 1520239200; // use `new Date().getTime()` + +// Desired schema, adapt to the newest version and add migrations to the array below const schema = [ - // Schema version #1 "CREATE TABLE IF NOT EXISTS options (name TEXT, value TEXT, CONSTRAINT name_unique UNIQUE (name))", "CREATE TABLE IF NOT EXISTS messages (network TEXT, channel TEXT, time INTEGER, type TEXT, msg TEXT)", "CREATE INDEX IF NOT EXISTS network_channel ON messages (network, channel)", "CREATE INDEX IF NOT EXISTS time ON messages (time)", ]; +// the migrations will be executed in an exclusive transaction as a whole +export const migrations = []; + class Deferred { resolve!: () => void; promise: Promise; @@ -326,4 +331,8 @@ function parseSearchRowsToMessages(id: number, rows: any[]) { return messages; } +export function necessaryMigrations(since: number): Migration[] { + return migrations.filter((m) => m.version > since); +} + export default SqliteMessageStorage; diff --git a/test/plugins/sqlite.ts b/test/plugins/sqlite.ts index 5f0d8b0a..e2723872 100644 --- a/test/plugins/sqlite.ts +++ b/test/plugins/sqlite.ts @@ -5,7 +5,102 @@ import {expect} from "chai"; import util from "../util"; import Msg, {MessageType} from "../../server/models/msg"; import Config from "../../server/config"; -import MessageStorage, {currentSchemaVersion} from "../../server/plugins/messageStorage/sqlite"; +import MessageStorage, { + currentSchemaVersion, + migrations, + necessaryMigrations, +} from "../../server/plugins/messageStorage/sqlite"; +import Client from "../../server/client"; +import sqlite3 from "sqlite3"; + +const orig_schema = [ + // Schema version #1 + // DO NOT CHANGE THIS IN ANY WAY, it's needed to properly test migrations + "CREATE TABLE IF NOT EXISTS options (name TEXT, value TEXT, CONSTRAINT name_unique UNIQUE (name))", + "CREATE TABLE IF NOT EXISTS messages (network TEXT, channel TEXT, time INTEGER, type TEXT, msg TEXT)", + "CREATE INDEX IF NOT EXISTS network_channel ON messages (network, channel)", + "CREATE INDEX IF NOT EXISTS time ON messages (time)", +]; + +const v1_schema_version = 1520239200; + +const v1_dummy_messages = [ + { + network: "8f650427-79a2-4950-b8af-94088b61b37c", + channel: "##linux", + time: 1594845354280, + type: "message", + msg: '{"from":{"mode":"","nick":"rascul"},"text":"db on a flash drive doesn\'t sound very nice though","self":false,"highlight":false,"users":[]}', + }, + { + network: "8f650427-79a2-4950-b8af-94088b61b37c", + channel: "##linux", + time: 1594845357234, + type: "message", + msg: '{"from":{"mode":"","nick":"GrandPa-G"},"text":"that\'s the point of changing to make sure.","self":false,"highlight":false,"users":[]}', + }, + { + network: "8f650427-79a2-4950-b8af-94088b61b37c", + channel: "#pleroma-dev", + time: 1594845358464, + type: "message", + msg: '{"from":{"mode":"@","nick":"rinpatch"},"text":"it\'s complicated","self":false,"highlight":false,"users":[]}', + }, +]; + +describe("SQLite migrations", function () { + let db: sqlite3.Database; + + function serialize_run(stmt: string, ...params: any[]): Promise { + return new Promise((resolve, reject) => { + db.serialize(() => { + db.run(stmt, params, (err) => { + if (err) { + reject(err); + return; + } + + resolve(); + }); + }); + }); + } + + before(async function () { + db = new sqlite3.Database(":memory:"); + + for (const stmt of orig_schema) { + await serialize_run(stmt); + } + + for (const msg of v1_dummy_messages) { + await serialize_run( + "INSERT INTO messages(network, channel, time, type, msg) VALUES(?, ?, ?, ?, ?)", + msg.network, + msg.channel, + msg.time, + msg.type, + msg.msg + ); + } + }); + + after(function (done) { + db.close(done); + }); + + it("has working migrations", async function () { + const to_execute = necessaryMigrations(v1_schema_version); + expect(to_execute.length).to.eq(migrations.length); + await serialize_run("BEGIN EXCLUSIVE TRANSACTION"); + + for (const stmt of to_execute.map((m) => m.stmts).flat()) { + await serialize_run(stmt); + } + + await serialize_run("COMMIT TRANSACTION"); + }); +}); describe("SQLite Message Storage", function () { // Increase timeout due to unpredictable I/O on CI services