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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,21 @@
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 final AuctionPlugin plugin;
private AuctionLoader auctionLoader;
private Repositories repositories;
Expand All @@ -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()) {

Expand All @@ -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);
Comment on lines +72 to 73
Copy link

Copilot AI Apr 6, 2026

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 +88,7 @@ public boolean onEnable() {
this.repositories.register(TransactionRepository.class);

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

return true;
}
Expand Down Expand Up @@ -239,4 +251,63 @@ 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) {
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();
Comment on lines +274 to +292
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

ensureRequiredTablesExist() opens a new JDBC connection and fetches metadata for each required table. On pooled DBs this is extra overhead during startup; on SQLite it can also increase file locking churn. Consider reusing a single Connection/DatabaseMetaData for the whole check to minimize connections and repeated metadata lookups.

Suggested change
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();
try (Connection connection = this.databaseConnection.getConnection()) {
DatabaseMetaData metaData = connection.getMetaData();
if (metaData == null) {
this.plugin.getLogger().warning("Failed to inspect required tables: database metadata is unavailable.");
return;
}
StorageSchemaDefinitions.requiredTables().forEach((tableName, definition) -> {
if (tableExists(connection, metaData, 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);
}
});
} catch (SQLException exception) {
throw new IllegalStateException("Failed to inspect required tables", exception);
}
}
private boolean tableExists(String tableName) {
try (Connection connection = this.databaseConnection.getConnection()) {
DatabaseMetaData metaData = connection.getMetaData();
return tableExists(connection, metaData, tableName);
} catch (SQLException exception) {
String resolvedTableName = this.databaseConnection.getDatabaseConfiguration().replacePrefix(tableName);
this.plugin.getLogger().warning("Failed to inspect table '" + resolvedTableName + "': " + exception.getMessage());
return false;
}
}
private boolean tableExists(Connection connection, DatabaseMetaData metaData, String tableName) {
String resolvedTableName = this.databaseConnection.getDatabaseConfiguration().replacePrefix(tableName);
try {

Copilot uses AI. Check for mistakes.
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 +301 to +305
Copy link

Copilot AI Apr 6, 2026

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