Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<String, Consumer<Schema>> requiredTables() {
Map<String, Consumer<Schema>> 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();
}
}
121 changes: 119 additions & 2 deletions src/main/java/fr/maxlego08/zauctionhouse/storage/ZStorageManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,22 @@
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;
import java.util.stream.Collectors;

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;
private Repositories repositories;
Expand All @@ -48,6 +57,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()) {

Expand All @@ -58,7 +70,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);
Comment on lines +73 to 74

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switching the migration tracking table name from the previously hard-coded value to a prefix-derived value can orphan existing installs' migration history (e.g., upgrades that already have the old migrations table). Consider adding a compatibility fallback (detect old table name and reuse/rename/copy it) or at least log clearly that migrations will be re-evaluated under a new tracking table to avoid unexpected behavior on upgrade.

Copilot uses AI. Check for mistakes.

MigrationManager.registerMigration(new CreatePlayerMigration());
Expand All @@ -77,6 +89,7 @@ public boolean onEnable() {
this.repositories.register(TransactionRepository.class);

MigrationManager.execute(this.databaseConnection, sarahLogger);
ensureRequiredTablesExist(sarahLogger);

return true;
}
Expand Down Expand Up @@ -239,4 +252,108 @@ public List<Item> selectItems(List<Integer> integers) {
public Map<UUID, String> selectPlayers(List<String> 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) {
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) {
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;
}

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) {
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;
}
Comment on lines +347 to +351

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tableExists() returns false on any SQLException while inspecting metadata, which makes ensureRequiredTablesExist() assume the table is missing and attempt a CREATE TABLE. If metadata inspection fails for driver/permission reasons, this can incorrectly try to recreate existing tables (and potentially fail startup). Consider treating inspection failure as a hard error (disable plugin) or skipping self-heal when table existence can't be determined reliably.

Copilot uses AI. Check for mistakes.
}

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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading