From ecd98ac9c579115779e79ac6fa9d9938005d4fe4 Mon Sep 17 00:00:00 2001 From: jarvis2f <137974272+jarvis2f@users.noreply.github.com> Date: Thu, 9 Jan 2025 12:00:50 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20Fix=20startup=20failure?= =?UTF-8?q?=20due=20to=20Sql=20migration=20(#10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/telegram/files/DataVerticle.java | 20 +- .../files/DataVerticleMigrationTest.java | 179 ++++++++++++++++++ 2 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 api/src/test/java/telegram/files/DataVerticleMigrationTest.java diff --git a/api/src/main/java/telegram/files/DataVerticle.java b/api/src/main/java/telegram/files/DataVerticle.java index b32ad32..b6ff3d7 100644 --- a/api/src/main/java/telegram/files/DataVerticle.java +++ b/api/src/main/java/telegram/files/DataVerticle.java @@ -53,11 +53,14 @@ public class DataVerticle extends AbstractVerticle { new StatisticRecord.StatisticRecordDefinition() ); pool.getConnection() - .compose(conn -> Future.all(definitions.stream().map(d -> d.createTable(conn)).toList()).map(conn)) - .compose(conn -> settingRepository.getByKey(SettingKey.version).map(v -> Tuple.tuple(conn, v))) + .compose(conn -> isCompletelyNewInitialization(conn).map(isNew -> Tuple.tuple(conn, isNew))) + .compose(tuple -> Future.all(definitions.stream().map(d -> d.createTable(tuple.v1)).toList()).map(tuple)) + .compose(tuple -> settingRepository.getByKey(SettingKey.version).map(tuple::concat)) .compose(tuple -> { + if (tuple.v2) return Future.succeededFuture(); + SqlConnection conn = tuple.v1; - Version version = tuple.v2 == null ? new Version("0.0.0") : tuple.v2; + Version version = tuple.v3 == null ? new Version("0.0.0") : tuple.v3; return Future.all(definitions.stream().map(d -> d.migrate(conn, version, new Version(Start.VERSION))).toList()); }) .compose(r -> @@ -92,4 +95,15 @@ public class DataVerticle extends AbstractVerticle { dataPath = Config.APP_ROOT + File.separator + dataPath; return dataPath; } + + private Future isCompletelyNewInitialization(SqlConnection conn) { + return conn.query(""" + SELECT name + FROM sqlite_master + WHERE type = 'table' + ORDER BY name; + """) + .execute() + .map(rs -> rs.size() == 0); + } } diff --git a/api/src/test/java/telegram/files/DataVerticleMigrationTest.java b/api/src/test/java/telegram/files/DataVerticleMigrationTest.java new file mode 100644 index 0000000..ad862db --- /dev/null +++ b/api/src/test/java/telegram/files/DataVerticleMigrationTest.java @@ -0,0 +1,179 @@ +package telegram.files; + + +import cn.hutool.core.collection.IterUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.lang.Version; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.jdbcclient.JDBCConnectOptions; +import io.vertx.jdbcclient.JDBCPool; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; +import io.vertx.sqlclient.PoolOptions; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.extension.ExtendWith; +import telegram.files.repository.SettingKey; + +import java.util.Set; +import java.util.stream.Collectors; + +@ExtendWith(VertxExtension.class) +public class DataVerticleMigrationTest { + + private static final Log log = LogFactory.get(); + + @BeforeAll + static void setUp() { + log.debug("APP_ROOT: " + Config.APP_ROOT); + log.debug("DATA_PATH:" + DataVerticle.getDataPath()); + } + + @AfterEach + void tearDown() { + String dataPath = DataVerticle.getDataPath(); + if (FileUtil.file(dataPath).exists()) { + FileUtil.del(dataPath); + } + } + + @Test + @DisplayName("Test new database initialization") + void testNewDatabaseInitialization(Vertx vertx, VertxTestContext testContext) { + initializeEmptyDatabase(vertx, testContext) + .compose(v -> DataVerticle.settingRepository.getByKey(SettingKey.version)) + .onComplete(testContext.succeeding(version -> testContext.verify(() -> { + Assertions.assertEquals(new Version(Start.VERSION), version); + testContext.completeNow(); + }))); + } + + @Test + @DisplayName("Test legacy database migration (pre-0.1.7, no version)") + void testLegacyDatabaseMigration(Vertx vertx, VertxTestContext testContext) { + initializeLegacyDatabase(vertx) + .compose(v -> vertx.deployVerticle(new DataVerticle())) + .compose(v -> DataVerticle.pool.getConnection()) + .compose(conn -> conn.query("PRAGMA table_info(file_record)").execute() + .compose(result -> { + testContext.verify(() -> { + // Verify whether there are new columns + Set columnNames = IterUtil.toList(result).stream() + .map(row -> row.getString("name")) + .collect(Collectors.toSet()); + Assertions.assertTrue(columnNames.contains("start_date")); + Assertions.assertTrue(columnNames.contains("completion_date")); + }); + return conn.close(); + })) + .compose(v -> DataVerticle.settingRepository.getByKey(SettingKey.version)) + .onComplete(testContext.succeeding(version -> testContext.verify(() -> { + Assertions.assertEquals(new Version(Start.VERSION), version); + testContext.completeNow(); + }))); + } + + @Test + @DisplayName("Test migration from 0.1.6 to current version") + void testOldVersionMigration(Vertx vertx, VertxTestContext testContext) { + initializeOldVersionDatabase(vertx) + .compose(v -> vertx.deployVerticle(new DataVerticle())) + .compose(v -> DataVerticle.pool.getConnection()) + .compose(conn -> conn.query("PRAGMA table_info(file_record)").execute() + .compose(result -> { + testContext.verify(() -> { + // Verify whether there are new columns + Set columnNames = IterUtil.toList(result).stream() + .map(row -> row.getString("name")) + .collect(Collectors.toSet()); + Assertions.assertTrue(columnNames.contains("start_date")); + Assertions.assertTrue(columnNames.contains("completion_date")); + }); + return conn.close(); + })) + .compose(v -> DataVerticle.settingRepository.getByKey(SettingKey.version)) + .onComplete(testContext.succeeding(version -> testContext.verify(() -> { + Assertions.assertEquals(new Version(Start.VERSION), version); + testContext.completeNow(); + }))); + } + + private Future initializeEmptyDatabase(Vertx vertx, VertxTestContext testContext) { + return vertx.deployVerticle(new DataVerticle()) + .compose(id -> { + // Verify that the database has been fully initialized + return DataVerticle.pool.getConnection() + .compose(conn -> conn.query(""" + SELECT name FROM sqlite_master + WHERE type='table' AND name IN ('setting_record', 'telegram_record', 'file_record', 'statistic_record') + """).execute() + .compose(result -> { + testContext.verify(() -> { + Assertions.assertEquals(4, result.size()); + }); + return conn.close(); + })); + }) + .mapEmpty(); + } + + private Future initializeLegacyDatabase(Vertx vertx) { + // Create an old version database without a version field + return Future.succeededFuture() + .compose(v -> { + JDBCPool tempPool = JDBCPool.pool(vertx, + new JDBCConnectOptions() + .setJdbcUrl("jdbc:sqlite:%s".formatted(DataVerticle.getDataPath())), + new PoolOptions().setMaxSize(1).setName("temp-pool") + ); + return tempPool.getConnection() + .compose(conn -> conn.query(""" + CREATE TABLE file_record ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + file_id TEXT NOT NULL, + file_name TEXT NOT NULL + ) + """).execute() + .compose(v2 -> conn.query(""" + INSERT INTO file_record (file_id, file_name) + VALUES ('123', 'test.txt') + """).execute()) + .compose(v3 -> conn.close()) + .eventually(() -> tempPool.close())); + }); + } + + private Future initializeOldVersionDatabase(Vertx vertx) { + // Create a database with a version field but a lower version + return Future.succeededFuture() + .compose(v -> { + JDBCPool tempPool = JDBCPool.pool(vertx, + new JDBCConnectOptions() + .setJdbcUrl("jdbc:sqlite:%s".formatted(DataVerticle.getDataPath())), + new PoolOptions().setMaxSize(1).setName("temp-pool") + ); + return tempPool.getConnection() + .compose(conn -> conn.query(""" + CREATE TABLE setting_record ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) + """).execute() + .compose(v2 -> conn.query(""" + INSERT INTO setting_record (key, value) + VALUES ('version', '0.1.6') + """).execute()) + .compose(v3 -> conn.query(""" + CREATE TABLE file_record ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + file_id TEXT NOT NULL, + file_name TEXT NOT NULL + ) + """).execute()) + .compose(v4 -> conn.close()) + .eventually(() -> tempPool.close())); + }); + } +}