From 7049ebdaa8d2ea68aac873557a0c29fef3d625cc Mon Sep 17 00:00:00 2001 From: SaltyAlpaca Date: Sat, 4 Apr 2026 14:17:08 +0200 Subject: [PATCH 1/2] Fix SQLite schema bootstrap and legacy database detection --- .../storage/StorageSchemaDefinitions.java | 77 +++++++++++++++++++ .../storage/ZStorageManager.java | 73 +++++++++++++++++- .../CreateAuctionItemMigration.java | 8 +- .../migrations/CreateItemMigration.java | 15 +--- .../migrations/CreateLogsMigration.java | 15 +--- .../migrations/CreatePlayerMigration.java | 7 +- .../CreateTransactionsMigration.java | 13 +--- 7 files changed, 159 insertions(+), 49 deletions(-) create mode 100644 src/main/java/fr/maxlego08/zauctionhouse/storage/StorageSchemaDefinitions.java diff --git a/src/main/java/fr/maxlego08/zauctionhouse/storage/StorageSchemaDefinitions.java b/src/main/java/fr/maxlego08/zauctionhouse/storage/StorageSchemaDefinitions.java new file mode 100644 index 0000000..9d4b748 --- /dev/null +++ b/src/main/java/fr/maxlego08/zauctionhouse/storage/StorageSchemaDefinitions.java @@ -0,0 +1,77 @@ +package fr.maxlego08.zauctionhouse.storage; + +import fr.maxlego08.sarah.database.Schema; +import fr.maxlego08.zauctionhouse.api.item.StorageType; +import fr.maxlego08.zauctionhouse.api.storage.Tables; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Consumer; + +public final class StorageSchemaDefinitions { + + private StorageSchemaDefinitions() { + } + + public static Map> requiredTables() { + Map> tables = new LinkedHashMap<>(); + tables.put(Tables.PLAYERS, StorageSchemaDefinitions::players); + tables.put(Tables.ITEMS, StorageSchemaDefinitions::items); + tables.put(Tables.AUCTION_ITEMS, StorageSchemaDefinitions::auctionItems); + tables.put(Tables.TRANSACTIONS, StorageSchemaDefinitions::transactions); + tables.put(Tables.LOGS, StorageSchemaDefinitions::logs); + return tables; + } + + public static void players(Schema table) { + table.uuid("unique_id").primary().unique(); + table.string("name", 16); + table.timestamps(); + } + + public static void items(Schema table) { + table.autoIncrement("id"); + table.string("item_type", 255); + table.string("seller_unique_id", 36).foreignKey(Tables.PLAYERS, "unique_id", true); + table.string("buyer_unique_id", 36).nullable().foreignKey(Tables.PLAYERS, "unique_id", true); + table.decimal("price", 65, 2); + table.string("economy_name", 255); + table.enumType("storage_type", StorageType.class); + table.string("server_name", 255); + table.timestamp("expired_at"); + table.timestamps(); + } + + public static void auctionItems(Schema table) { + table.autoIncrement("id"); + table.integer("item_id").foreignKey(Tables.ITEMS, "id", true); + table.longText("itemstack"); + table.timestamps(); + } + + public static void transactions(Schema table) { + table.autoIncrement("id"); + table.integer("item_id").foreignKey(Tables.ITEMS, "id", true); + table.string("player_unique_id", 36).foreignKey(Tables.PLAYERS, "unique_id", true); + table.string("economy_name", 255); + table.decimal("before", 65, 2); + table.decimal("after", 65, 2); + table.decimal("value", 65, 2); + table.string("status", 32); + table.timestamps(); + } + + public static void logs(Schema table) { + table.autoIncrement("id"); + table.integer("item_id").foreignKey(Tables.ITEMS, "id", true); + table.string("log_type", 255); + table.string("player_unique_id", 36).foreignKey(Tables.PLAYERS, "unique_id", true); + table.string("target_unique_id", 36).nullable().foreignKey(Tables.PLAYERS, "unique_id", true); + table.longText("itemstack").nullable(); + table.decimal("price", 65, 2).defaultValue(0); + table.string("economy_name", 255).nullable(); + table.longText("additional_data").nullable(); + table.timestamp("readed_at").nullable(); + table.timestamps(); + } +} diff --git a/src/main/java/fr/maxlego08/zauctionhouse/storage/ZStorageManager.java b/src/main/java/fr/maxlego08/zauctionhouse/storage/ZStorageManager.java index ae60c88..6a329ae 100644 --- a/src/main/java/fr/maxlego08/zauctionhouse/storage/ZStorageManager.java +++ b/src/main/java/fr/maxlego08/zauctionhouse/storage/ZStorageManager.java @@ -25,6 +25,11 @@ import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; +import java.io.File; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; import java.math.BigDecimal; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -32,6 +37,9 @@ public class ZStorageManager extends ItemLoaderUtils implements StorageManager { + private static final String SQLITE_DATABASE_FILE = "database.db"; + private static final String SQLITE_LEGACY_DATABASE_FILE = "storage.db"; + private final AuctionPlugin plugin; private AuctionLoader auctionLoader; private Repositories repositories; @@ -48,6 +56,9 @@ public boolean onEnable() { var databaseConfiguration = this.getDatabaseConfiguration(); var isSqlite = databaseConfiguration.getDatabaseType() == DatabaseType.SQLITE; this.databaseConnection = isSqlite ? new SqliteConnection(databaseConfiguration, this.plugin.getDataFolder(), sarahLogger) : new HikariDatabaseConnection(databaseConfiguration, sarahLogger); + if (isSqlite && this.databaseConnection instanceof SqliteConnection sqliteConnection) { + configureSqliteFile(sqliteConnection); + } if (!databaseConnection.isValid()) { @@ -58,7 +69,7 @@ public boolean onEnable() { this.plugin.getLogger().info("The database connection is valid !"); } - MigrationManager.setMigrationTableName("zauctionhousev4_migrations"); + MigrationManager.setMigrationTableName(resolveMigrationTableName(databaseConfiguration.getTablePrefix())); MigrationManager.setDatabaseConfiguration(databaseConfiguration); MigrationManager.registerMigration(new CreatePlayerMigration()); @@ -77,6 +88,7 @@ public boolean onEnable() { this.repositories.register(TransactionRepository.class); MigrationManager.execute(this.databaseConnection, sarahLogger); + ensureRequiredTablesExist(sarahLogger); return true; } @@ -239,4 +251,63 @@ public List selectItems(List integers) { public Map selectPlayers(List uuids) { return with(PlayerRepository.class).select(uuids).stream().collect(Collectors.toMap(PlayerDTO::unique_id, PlayerDTO::name)); } + + private void configureSqliteFile(SqliteConnection sqliteConnection) { + File dataFolder = this.plugin.getDataFolder(); + File currentFile = new File(dataFolder, SQLITE_DATABASE_FILE); + File legacyFile = new File(dataFolder, SQLITE_LEGACY_DATABASE_FILE); + + if (legacyFile.exists() && !currentFile.exists()) { + sqliteConnection.setFileName(SQLITE_LEGACY_DATABASE_FILE); + this.plugin.getLogger().info("Using legacy SQLite database file: " + SQLITE_LEGACY_DATABASE_FILE); + return; + } + + sqliteConnection.setFileName(SQLITE_DATABASE_FILE); + } + + private String resolveMigrationTableName(String tablePrefix) { + return (tablePrefix == null ? "" : tablePrefix) + "migrations"; + } + + private void ensureRequiredTablesExist(fr.maxlego08.sarah.logger.Logger logger) { + StorageSchemaDefinitions.requiredTables().forEach((tableName, definition) -> { + if (tableExists(tableName)) { + return; + } + + String resolvedTableName = this.databaseConnection.getDatabaseConfiguration().replacePrefix(tableName); + this.plugin.getLogger().warning("Missing required table '" + resolvedTableName + "'. Recreating it now."); + try { + SchemaBuilder.create(null, tableName, definition).execute(this.databaseConnection, logger); + } catch (SQLException exception) { + throw new IllegalStateException("Failed to recreate missing table '" + resolvedTableName + "'", exception); + } + }); + } + + private boolean tableExists(String tableName) { + String resolvedTableName = this.databaseConnection.getDatabaseConfiguration().replacePrefix(tableName); + try (Connection connection = this.databaseConnection.getConnection()) { + DatabaseMetaData metaData = connection.getMetaData(); + if (metaData == null) { + return false; + } + + if (hasTable(metaData, connection, resolvedTableName)) { + return true; + } + + return hasTable(metaData, connection, resolvedTableName.toUpperCase(Locale.ROOT)); + } catch (SQLException exception) { + this.plugin.getLogger().warning("Failed to inspect table '" + resolvedTableName + "': " + exception.getMessage()); + return false; + } + } + + private boolean hasTable(DatabaseMetaData metaData, Connection connection, String tableName) throws SQLException { + try (ResultSet resultSet = metaData.getTables(connection.getCatalog(), null, tableName, new String[]{"TABLE"})) { + return resultSet.next(); + } + } } diff --git a/src/main/java/fr/maxlego08/zauctionhouse/storage/migrations/CreateAuctionItemMigration.java b/src/main/java/fr/maxlego08/zauctionhouse/storage/migrations/CreateAuctionItemMigration.java index 26789a0..0ab8183 100644 --- a/src/main/java/fr/maxlego08/zauctionhouse/storage/migrations/CreateAuctionItemMigration.java +++ b/src/main/java/fr/maxlego08/zauctionhouse/storage/migrations/CreateAuctionItemMigration.java @@ -2,16 +2,12 @@ import fr.maxlego08.sarah.database.Migration; import fr.maxlego08.zauctionhouse.api.storage.Tables; +import fr.maxlego08.zauctionhouse.storage.StorageSchemaDefinitions; public class CreateAuctionItemMigration extends Migration { @Override public void up() { - create(Tables.AUCTION_ITEMS, table -> { - table.autoIncrement("id"); - table.integer("item_id").foreignKey(Tables.ITEMS, "id", true); - table.longText("itemstack"); - table.timestamps(); - }); + create(Tables.AUCTION_ITEMS, StorageSchemaDefinitions::auctionItems); } } diff --git a/src/main/java/fr/maxlego08/zauctionhouse/storage/migrations/CreateItemMigration.java b/src/main/java/fr/maxlego08/zauctionhouse/storage/migrations/CreateItemMigration.java index ad2912e..21f3601 100644 --- a/src/main/java/fr/maxlego08/zauctionhouse/storage/migrations/CreateItemMigration.java +++ b/src/main/java/fr/maxlego08/zauctionhouse/storage/migrations/CreateItemMigration.java @@ -1,24 +1,13 @@ package fr.maxlego08.zauctionhouse.storage.migrations; import fr.maxlego08.sarah.database.Migration; -import fr.maxlego08.zauctionhouse.api.item.StorageType; import fr.maxlego08.zauctionhouse.api.storage.Tables; +import fr.maxlego08.zauctionhouse.storage.StorageSchemaDefinitions; public class CreateItemMigration extends Migration { @Override public void up() { - create(Tables.ITEMS, table -> { - table.autoIncrement("id"); - table.string("item_type", 255); - table.string("seller_unique_id", 36).foreignKey(Tables.PLAYERS, "unique_id", true); - table.string("buyer_unique_id", 36).nullable().foreignKey(Tables.PLAYERS, "unique_id", true); - table.decimal("price", 65, 2); - table.string("economy_name", 255); - table.enumType("storage_type", StorageType.class); - table.string("server_name", 255); - table.timestamp("expired_at"); - table.timestamps(); - }); + create(Tables.ITEMS, StorageSchemaDefinitions::items); } } diff --git a/src/main/java/fr/maxlego08/zauctionhouse/storage/migrations/CreateLogsMigration.java b/src/main/java/fr/maxlego08/zauctionhouse/storage/migrations/CreateLogsMigration.java index 4486413..48658a5 100644 --- a/src/main/java/fr/maxlego08/zauctionhouse/storage/migrations/CreateLogsMigration.java +++ b/src/main/java/fr/maxlego08/zauctionhouse/storage/migrations/CreateLogsMigration.java @@ -2,23 +2,12 @@ import fr.maxlego08.sarah.database.Migration; import fr.maxlego08.zauctionhouse.api.storage.Tables; +import fr.maxlego08.zauctionhouse.storage.StorageSchemaDefinitions; public class CreateLogsMigration extends Migration { @Override public void up() { - create(Tables.LOGS, table -> { - table.autoIncrement("id"); - table.integer("item_id").foreignKey(Tables.ITEMS, "id", true); - table.string("log_type", 255); - table.string("player_unique_id", 36).foreignKey(Tables.PLAYERS, "unique_id", true); - table.string("target_unique_id", 36).nullable().foreignKey(Tables.PLAYERS, "unique_id", true); - table.longText("itemstack").nullable(); - table.decimal("price", 65, 2).defaultValue(0); - table.string("economy_name", 255).nullable(); - table.longText("additional_data").nullable(); - table.timestamp("readed_at").nullable(); - table.timestamps(); - }); + create(Tables.LOGS, StorageSchemaDefinitions::logs); } } diff --git a/src/main/java/fr/maxlego08/zauctionhouse/storage/migrations/CreatePlayerMigration.java b/src/main/java/fr/maxlego08/zauctionhouse/storage/migrations/CreatePlayerMigration.java index 5e528a6..43a3149 100644 --- a/src/main/java/fr/maxlego08/zauctionhouse/storage/migrations/CreatePlayerMigration.java +++ b/src/main/java/fr/maxlego08/zauctionhouse/storage/migrations/CreatePlayerMigration.java @@ -1,16 +1,13 @@ package fr.maxlego08.zauctionhouse.storage.migrations; import fr.maxlego08.sarah.database.Migration; +import fr.maxlego08.zauctionhouse.storage.StorageSchemaDefinitions; import fr.maxlego08.zauctionhouse.api.storage.Tables; public class CreatePlayerMigration extends Migration { @Override public void up() { - create(Tables.PLAYERS, table -> { - table.uuid("unique_id").primary().unique(); - table.string("name", 16); - table.timestamps(); - }); + create(Tables.PLAYERS, StorageSchemaDefinitions::players); } } diff --git a/src/main/java/fr/maxlego08/zauctionhouse/storage/migrations/CreateTransactionsMigration.java b/src/main/java/fr/maxlego08/zauctionhouse/storage/migrations/CreateTransactionsMigration.java index 4a6340a..624d099 100644 --- a/src/main/java/fr/maxlego08/zauctionhouse/storage/migrations/CreateTransactionsMigration.java +++ b/src/main/java/fr/maxlego08/zauctionhouse/storage/migrations/CreateTransactionsMigration.java @@ -2,21 +2,12 @@ import fr.maxlego08.sarah.database.Migration; import fr.maxlego08.zauctionhouse.api.storage.Tables; +import fr.maxlego08.zauctionhouse.storage.StorageSchemaDefinitions; public class CreateTransactionsMigration extends Migration { @Override public void up() { - create(Tables.TRANSACTIONS, table -> { - table.autoIncrement("id"); - table.integer("item_id").foreignKey(Tables.ITEMS, "id", true); - table.string("player_unique_id", 36).foreignKey(Tables.PLAYERS, "unique_id", true); - table.string("economy_name", 255); - table.decimal("before", 65, 2); - table.decimal("after", 65, 2); - table.decimal("value", 65, 2); - table.string("status", 32); - table.timestamps(); - }); + create(Tables.TRANSACTIONS, StorageSchemaDefinitions::transactions); } } From 4411e859bcab7e6b8823f1f0f068d19370377e63 Mon Sep 17 00:00:00 2001 From: SaltyAlpaca Date: Fri, 10 Apr 2026 14:59:04 +0200 Subject: [PATCH 2/2] Update ZStorageManager.java --- .../storage/ZStorageManager.java | 70 +++++++++++++++---- 1 file changed, 58 insertions(+), 12 deletions(-) diff --git a/src/main/java/fr/maxlego08/zauctionhouse/storage/ZStorageManager.java b/src/main/java/fr/maxlego08/zauctionhouse/storage/ZStorageManager.java index 6a329ae..da67bcd 100644 --- a/src/main/java/fr/maxlego08/zauctionhouse/storage/ZStorageManager.java +++ b/src/main/java/fr/maxlego08/zauctionhouse/storage/ZStorageManager.java @@ -39,6 +39,7 @@ public class ZStorageManager extends ItemLoaderUtils implements StorageManager { private static final String SQLITE_DATABASE_FILE = "database.db"; private static final String SQLITE_LEGACY_DATABASE_FILE = "storage.db"; + private static final String LEGACY_MIGRATION_TABLE = "zauctionhousev4_migrations"; private final AuctionPlugin plugin; private AuctionLoader auctionLoader; @@ -267,27 +268,72 @@ private void configureSqliteFile(SqliteConnection sqliteConnection) { } private String resolveMigrationTableName(String tablePrefix) { - return (tablePrefix == null ? "" : tablePrefix) + "migrations"; + String newName = (tablePrefix == null ? "" : tablePrefix) + "migrations"; + + // If the legacy table still exists but the new prefix-derived table does not, + // keep using the legacy name so existing migration history is preserved. + if (rawTableExists(LEGACY_MIGRATION_TABLE) && !rawTableExists(newName)) { + this.plugin.getLogger().info( + "Found legacy migration table '" + LEGACY_MIGRATION_TABLE + + "'. Continuing to use it for backward compatibility."); + return LEGACY_MIGRATION_TABLE; + } + + return newName; } private void ensureRequiredTablesExist(fr.maxlego08.sarah.logger.Logger logger) { - StorageSchemaDefinitions.requiredTables().forEach((tableName, definition) -> { - if (tableExists(tableName)) { + try (Connection connection = this.databaseConnection.getConnection()) { + DatabaseMetaData metaData = connection.getMetaData(); + if (metaData == null) { + this.plugin.getLogger().warning("Database metadata is unavailable. Skipping table verification."); return; } - String resolvedTableName = this.databaseConnection.getDatabaseConfiguration().replacePrefix(tableName); - this.plugin.getLogger().warning("Missing required table '" + resolvedTableName + "'. Recreating it now."); - try { - SchemaBuilder.create(null, tableName, definition).execute(this.databaseConnection, logger); - } catch (SQLException exception) { - throw new IllegalStateException("Failed to recreate missing table '" + resolvedTableName + "'", exception); - } - }); + StorageSchemaDefinitions.requiredTables().forEach((tableName, definition) -> { + String resolvedTableName = this.databaseConnection.getDatabaseConfiguration().replacePrefix(tableName); + + boolean exists; + try { + exists = hasTable(metaData, connection, resolvedTableName) + || hasTable(metaData, connection, resolvedTableName.toUpperCase(Locale.ROOT)); + } catch (SQLException exception) { + throw new IllegalStateException( + "Cannot determine existence of table '" + resolvedTableName + "'. Aborting to prevent data corruption.", + exception); + } + + if (exists) { + return; + } + + this.plugin.getLogger().warning("Missing required table '" + resolvedTableName + "'. Recreating it now."); + try { + SchemaBuilder.create(null, tableName, definition).execute(this.databaseConnection, logger); + } catch (SQLException exception) { + throw new IllegalStateException("Failed to recreate missing table '" + resolvedTableName + "'", exception); + } + }); + } catch (SQLException exception) { + throw new IllegalStateException("Failed to obtain database connection for table verification", exception); + } } + /** + * Checks whether a table exists using the configured table prefix to resolve the name. + * Throws on inspection failure to prevent accidental table recreation. + */ private boolean tableExists(String tableName) { String resolvedTableName = this.databaseConnection.getDatabaseConfiguration().replacePrefix(tableName); + return rawTableExists(resolvedTableName); + } + + /** + * Checks whether a table exists using the exact name provided (no prefix resolution). + * Used by the migration fallback where the table name is already fully qualified. + * Returns false on inspection failure (non-destructive context) with a warning log. + */ + private boolean rawTableExists(String resolvedTableName) { try (Connection connection = this.databaseConnection.getConnection()) { DatabaseMetaData metaData = connection.getMetaData(); if (metaData == null) { @@ -310,4 +356,4 @@ private boolean hasTable(DatabaseMetaData metaData, Connection connection, Strin return resultSet.next(); } } -} +} \ No newline at end of file