diff --git a/Core/src/main/java/com/craftaro/core/SongodaPlugin.java b/Core/src/main/java/com/craftaro/core/SongodaPlugin.java index 7afed548..486f0b24 100644 --- a/Core/src/main/java/com/craftaro/core/SongodaPlugin.java +++ b/Core/src/main/java/com/craftaro/core/SongodaPlugin.java @@ -272,21 +272,12 @@ public abstract class SongodaPlugin extends JavaPlugin { * @param migrations List of migrations to run. */ protected void initDatabase(List migrations) { - boolean legacy = this.config.contains("MySQL"); - boolean isSQLite = !this.config.getBoolean("MySQL.Enabled", false); - if (legacy && isSQLite) { - this.config.set("MySQL", null); + File databaseFile = new File(getDataFolder(), getName().toLowerCase() + ".db"); + boolean legacy = databaseFile.exists(); + + if (legacy) { + getLogger().warning("SQLite detected, converting to H2..."); this.dataManager = new DataManager(this, migrations, DatabaseType.SQLITE); - } else if (legacy) { - //Copy creditental from old config to new config - this.databaseConfig.set("MySQL.Hostname", this.config.getString("MySQL.Hostname", "localhost")); - this.databaseConfig.set("MySQL.Port", this.config.getInt("MySQL.Port", 3306)); - this.databaseConfig.set("MySQL.Database", this.config.getString("MySQL.Database", "database")); - this.databaseConfig.set("MySQL.Username", this.config.getString("MySQL.Username", "username")); - this.databaseConfig.set("MySQL.Password", this.config.getString("MySQL.Password", "password")); - this.databaseConfig.set("MySQL.Pool Size", this.config.getInt("MySQL.Pool Size", 5)); - this.databaseConfig.set("MySQL.Use SSL", this.config.getBoolean("MySQL.Use SSL", false)); - this.dataManager = new DataManager(this, migrations); } else { this.dataManager = new DataManager(this, migrations); } @@ -294,10 +285,14 @@ public abstract class SongodaPlugin extends JavaPlugin { //Check if the type is SQLite if (dataManager.getDatabaseConnector().getType() == DatabaseType.SQLITE) { //Let's convert it to H2 - DataManager newDataManager = DataMigration.convert(this, DatabaseType.H2); - if (newDataManager != null && newDataManager.getDatabaseConnector().isInitialized()) { - //Set the new data manager - setDataManager(newDataManager); + try { + DataManager newDataManager = DataMigration.convert(this, DatabaseType.H2); + if (newDataManager != null && newDataManager.getDatabaseConnector().isInitialized()) { + //Set the new data manager + setDataManager(newDataManager); + } + } catch (Exception e) { + e.printStackTrace(); } } } @@ -312,7 +307,8 @@ public abstract class SongodaPlugin extends JavaPlugin { if (this.dataManager == dataManager) return; //Make sure to shut down the old data manager. if (this.dataManager != null) { - dataManager.shutdown(); + this.dataManager.shutdown(); + this.dataManager = null; } this.dataManager = dataManager; } diff --git a/Core/src/main/java/com/craftaro/core/database/DataManager.java b/Core/src/main/java/com/craftaro/core/database/DataManager.java index 13044f05..da09a58c 100644 --- a/Core/src/main/java/com/craftaro/core/database/DataManager.java +++ b/Core/src/main/java/com/craftaro/core/database/DataManager.java @@ -5,6 +5,8 @@ import com.craftaro.core.SongodaPlugin; import com.craftaro.core.configuration.Config; import org.bukkit.Bukkit; import org.jetbrains.annotations.NotNull; +import org.jooq.Condition; +import org.jooq.Field; import org.jooq.Query; import org.jooq.Record; import org.jooq.impl.DSL; @@ -19,11 +21,13 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; @@ -47,6 +51,22 @@ public class DataManager { @Deprecated private static final Map> queues = new HashMap<>(); + + + DataManager() { + this.databaseConfig = null; + this.plugin = null; + this.migrations = Collections.emptyList(); + this.databaseConnector = new H2Connector(); + } + + DataManager(DatabaseType type) { + this.databaseConfig = null; + this.plugin = null; + this.migrations = Collections.emptyList(); + this.databaseConnector = new SQLiteConnector(); + } + public DataManager(SongodaPlugin plugin, List migrations) { this.plugin = plugin; this.migrations = migrations; @@ -77,15 +97,7 @@ public class DataManager { break; } case "SQLITE": { - //Lets check if we have the sqlite file in the plugin folder - File databaseFile = new File(plugin.getDataFolder(), plugin.getName().toLowerCase()+".db"); - if (!databaseFile.exists()) { - //Lets start SQLite and it will be converted to H2 - this.databaseConnector = new SQLiteConnector(plugin); - } else { - //No need for conversion, lets use H2 instead - this.databaseConnector = new H2Connector(plugin); - } + this.databaseConnector = new SQLiteConnector(plugin); break; } default: { @@ -116,6 +128,9 @@ public class DataManager { * @return the prefix to be used by all table names */ public String getTablePrefix() { + if (this.plugin == null) { + return ""; + } return this.plugin.getDescription().getName().toLowerCase() + '_'; } @@ -226,7 +241,7 @@ public class DataManager { databaseConnector.connectDSL(context -> { context.insertInto(DSL.table(getTablePrefix() + data.getTableName())) .set(data.serialize()) - .onConflict(DSL.field("id")).doUpdate() + .onConflict(data.getId() != -1 ? DSL.field("id") : DSL.field("uuid")).doUpdate() .set(data.serialize()) .where(data.getId() != -1 ? DSL.field("id").eq(data.getId()) : DSL.field("uuid").eq(data.getUniqueId().toString())) .execute(); @@ -241,7 +256,7 @@ public class DataManager { databaseConnector.connectDSL(context -> { context.insertInto(DSL.table(getTablePrefix() + data.getTableName())) .set(data.serialize()) - .onConflict(DSL.field("id")).doUpdate() + .onConflict(data.getId() != -1 ? DSL.field("id") : DSL.field("uuid")).doUpdate() .set(data.serialize()) .where(data.getId() != -1 ? DSL.field("id").eq(data.getId()) : DSL.field("uuid").eq(data.getUniqueId().toString())) .execute(); @@ -258,7 +273,7 @@ public class DataManager { for (Data data : dataBatch) { queries.add(context.insertInto(DSL.table(getTablePrefix() + data.getTableName())) .set(data.serialize()) - .onConflict(DSL.field("id")).doUpdate() + .onConflict(data.getId() != -1 ? DSL.field("id") : DSL.field("uuid")).doUpdate() .set(data.serialize()) .where(data.getId() != -1 ? DSL.field("id").eq(data.getId()) : DSL.field("uuid").eq(data.getUniqueId().toString()))); } @@ -277,7 +292,7 @@ public class DataManager { for (Data data : dataBatch) { queries.add(context.insertInto(DSL.table(getTablePrefix() + data.getTableName())) .set(data.serialize()) - .onConflict(DSL.field("id")).doUpdate() + .onConflict(data.getId() != -1 ? DSL.field("id") : DSL.field("uuid")).doUpdate() .set(data.serialize()) .where(data.getId() != -1 ? DSL.field("id").eq(data.getId()) : DSL.field("uuid").eq(data.getUniqueId().toString()))); } @@ -308,19 +323,25 @@ public class DataManager { public T load(int id, Class clazz, String table) { try { AtomicReference data = new AtomicReference<>((Data) clazz.getConstructor().newInstance()); + AtomicBoolean found = new AtomicBoolean(false); databaseConnector.connectDSL(context -> { try { - data.set((Data) clazz.getDeclaredConstructor().newInstance()); data.get().deserialize(Objects.requireNonNull(context.select() .from(DSL.table(getTablePrefix() + table)) .where(DSL.field("id").eq(id)) .fetchOne()) .intoMap()); + found.set(true); + } catch (NullPointerException ignored) { } catch (Exception ex) { ex.printStackTrace(); } }); - return (T) data.get(); + if (found.get()) { + return (T) data.get(); + } else { + return null; + } } catch (Exception ex) { throw new RuntimeException(ex); } @@ -329,6 +350,8 @@ public class DataManager { /** * Loads the data from the database * @param uuid The uuid of the data + * @param clazz The class of the data + * @param table The table of the data without prefix * @return The loaded data */ @SuppressWarnings("unchecked") @@ -346,7 +369,42 @@ public class DataManager { .intoMap()); found.set(true); } catch (NullPointerException ignored) { + } catch (Exception ex) { + ex.printStackTrace(); + } + }); + if (found.get()) { + return (T) data.get(); + } else { + return null; + } + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + /** + * Loads the data from the database + * @param uuid The uuid of the data + * @param clazz The class of the data + * @param table The table of the data without prefix + * @param uuidColumn The column of the uuid + * @return The loaded data + */ + @SuppressWarnings("unchecked") + public T load(UUID uuid, Class clazz, String table, String uuidColumn) { + try { + AtomicReference data = new AtomicReference<>((Data) clazz.getConstructor().newInstance()); + AtomicBoolean found = new AtomicBoolean(false); + databaseConnector.connectDSL(context -> { + try { + data.get().deserialize(Objects.requireNonNull(context.select() + .from(DSL.table(table)) + .where(DSL.field(uuidColumn).eq(uuid.toString())) + .fetchOne()) + .intoMap()); + found.set(true); + } catch (NullPointerException ignored) { } catch (Exception ex) { ex.printStackTrace(); } diff --git a/Core/src/main/java/com/craftaro/core/database/DataMigration.java b/Core/src/main/java/com/craftaro/core/database/DataMigration.java index 87db60ce..268139ab 100644 --- a/Core/src/main/java/com/craftaro/core/database/DataMigration.java +++ b/Core/src/main/java/com/craftaro/core/database/DataMigration.java @@ -5,9 +5,13 @@ import com.craftaro.core.SongodaPlugin; import java.io.File; import java.sql.Connection; +import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.sql.ResultSetMetaData; import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -33,7 +37,7 @@ public abstract class DataMigration { * @param toType The new database type * @return The new data manager instance */ - public static DataManager convert(SongodaPlugin plugin, DatabaseType toType) { + public static DataManager convert(SongodaPlugin plugin, DatabaseType toType) throws Exception { DataManager from = plugin.getDataManager(); if (from.getDatabaseConnector().getType() == toType) { plugin.getLogger().severe("Cannot convert to the same database type!"); @@ -45,65 +49,117 @@ public abstract class DataMigration { return null; } + plugin.getLogger().info("Converting data from " + from.getDatabaseConnector().getType().name() + " to " + toType.name() + "..."); DatabaseConnector fromConnector = from.getDatabaseConnector(); DatabaseConnector toConnector = to.getDatabaseConnector(); - Connection fromConnection; - Connection toConnection = null; + Connection fromConnection = fromConnector.getConnection(); + Connection toConnection = toConnector.getConnection(); try { - fromConnection = fromConnector.getConnection(); - toConnection = toConnector.getConnection(); - toConnection.setAutoCommit(false); - // Retrieve the list of tables from the old database - List tableNames = new ArrayList<>(); - try (ResultSet rs = fromConnection.getMetaData().getTables(null, null, null, new String[] {"TABLE"})) { - while (rs.next()) { - String tableName = rs.getString("TABLE_NAME"); - tableNames.add(tableName); - } - } + // Export schema + DatabaseMetaData meta = fromConnection.getMetaData(); + ResultSet tables = meta.getTables(null, null, null, new String[]{"TABLE"}); - // Transfer the data from the old database to the new database - for (String tableName : tableNames) { - try ( - PreparedStatement fromStmt = fromConnection.prepareStatement("SELECT * FROM " + tableName); - ResultSet rs = fromStmt.executeQuery(); - PreparedStatement toStmt = toConnection.prepareStatement("INSERT INTO " + tableName + " VALUES (" + String.join(",", Collections.nCopies(rs.getMetaData().getColumnCount(), "?")) + ")") - ) { - while (rs.next()) { - for (int i = 1; i <= rs.getMetaData().getColumnCount(); i++) { - toStmt.setObject(i, rs.getObject(i)); - } - toStmt.executeUpdate(); + while (tables.next()) { + String tableName = tables.getString("TABLE_NAME"); + Statement stmt = fromConnection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName); + + ResultSetMetaData metaRs = rs.getMetaData(); + int columnCount = metaRs.getColumnCount(); + + StringBuilder createTableQuery = new StringBuilder(); + createTableQuery.append("CREATE TABLE IF NOT EXISTS ").append(tableName).append(" ("); + + for (int i = 1; i <= columnCount; i++) { + String columnName = metaRs.getColumnName(i); + String columnType = metaRs.getColumnTypeName(i); + int columnSize = metaRs.getColumnDisplaySize(i); + + createTableQuery.append(columnName).append(" ").append(columnType).append("(").append(columnSize).append(")"); + + if (i < columnCount) { + createTableQuery.append(", "); } } + + createTableQuery.append(")"); + + toConnection.createStatement().execute(createTableQuery.toString()); + + while (rs.next()) { + StringBuilder insertQuery = new StringBuilder(); + insertQuery.append("INSERT INTO ").append(tableName).append(" VALUES ("); + + for (int i = 1; i <= columnCount; i++) { + Object value = rs.getObject(i); + + if (value == null) { + insertQuery.append("NULL"); + } else if (value instanceof String || value instanceof Timestamp) { + insertQuery.append("'").append(value).append("'"); + } else { + insertQuery.append(value); + } + + if (i < columnCount) { + insertQuery.append(", "); + } + } + + insertQuery.append(")"); + toConnection.createStatement().execute(insertQuery.toString()); + } } toConnection.commit(); + plugin.getLogger().info("Successfully migrated data from " + from.getDatabaseConnector().getType() + " to " + to.getDatabaseConnector().getType()); } catch (Exception e) { - if (toConnection != null) + if (toConnection != null) { try { toConnection.rollback(); } catch (SQLException e1) { e1.printStackTrace(); - SongodaCore.getInstance().getLogger().severe("Failed to rollback data for the new database"); + plugin.getLogger().severe("Failed to rollback data for the new database"); } + } e.printStackTrace(); - SongodaCore.getInstance().getLogger().severe("Failed to migrate data from " + from.getDatabaseConnector().getType() + " to " + to.getDatabaseConnector().getType()); + plugin.getLogger().severe("Failed to migrate data from " + from.getDatabaseConnector().getType() + " to " + to.getDatabaseConnector().getType()); return null; - } finally { - SongodaCore.getInstance().getLogger().info("Successfully migrated data from " + from.getDatabaseConnector().getType() + " to " + to.getDatabaseConnector().getType()); } fromConnector.closeConnection(); - //Get rid of the old database file + //Get rid of the old SQLite database file if it exists and create a backup File databaseFile = new File(plugin.getDataFolder(), plugin.getName().toLowerCase()+".db"); if (databaseFile.exists()) { //rename it to .old - databaseFile.renameTo(new File(plugin.getDataFolder(), plugin.getName().toLowerCase()+".old")); + databaseFile.renameTo(new File(plugin.getDataFolder(), plugin.getName().toLowerCase() + ".db.old")); + plugin.getLogger().info("Old database file renamed to " + plugin.getName().toLowerCase() + ".db.old"); } return to; } + + private String getTableColumns(Connection connection, String tableName) { + StringBuilder columns = new StringBuilder(); + try { + DatabaseMetaData meta = connection.getMetaData(); + ResultSet rs = meta.getColumns(null, null, tableName, null); + + while (rs.next()) { + String columnName = rs.getString("COLUMN_NAME"); + String columnType = rs.getString("TYPE_NAME"); + + columns.append(columnName).append(" ").append(columnType).append(", "); + } + + rs.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + + columns.setLength(columns.length() - 2); + return columns.toString(); + } } diff --git a/Core/src/main/java/com/craftaro/core/database/H2Connector.java b/Core/src/main/java/com/craftaro/core/database/H2Connector.java index ecb3d240..204c20a3 100644 --- a/Core/src/main/java/com/craftaro/core/database/H2Connector.java +++ b/Core/src/main/java/com/craftaro/core/database/H2Connector.java @@ -18,6 +18,29 @@ public class H2Connector implements DatabaseConnector { private HikariDataSource hikari; private boolean initializedSuccessfully; + H2Connector() { + this.plugin = null; + + int poolSize = 2; + String password = "password"; + String username = "username"; + + HikariConfig config = new HikariConfig(); + config.setDriverClassName("org.h2.Driver"); + config.setJdbcUrl("jdbc:h2:./db_test/CraftaroCoreTest;AUTO_RECONNECT=TRUE;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE"); + config.setPassword(username); + config.setUsername(password); + config.setMaximumPoolSize(poolSize); + + try { + this.hikari = new HikariDataSource(config); + this.initializedSuccessfully = true; + } catch (Exception ex) { + ex.printStackTrace(); + this.initializedSuccessfully = false; + } + } + public H2Connector(SongodaPlugin plugin) { this(plugin, plugin.getDatabaseConfig()); } @@ -31,7 +54,7 @@ public class H2Connector implements DatabaseConnector { HikariConfig config = new HikariConfig(); config.setDriverClassName("com.craftaro.core.third_party.org.h2.Driver"); - config.setJdbcUrl("jdbc:h2:./h2_" + plugin.getDataFolder().getPath().replaceAll("\\\\", "/") + "/" + plugin.getDescription().getName().toLowerCase()+ ";AUTO_RECONNECT=TRUE;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE"); + config.setJdbcUrl("jdbc:h2:./" + plugin.getDataFolder().getPath().replaceAll("\\\\", "/") + "/" + plugin.getDescription().getName().toLowerCase()+ ";AUTO_RECONNECT=TRUE;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE"); config.setPassword(username); config.setUsername(password); config.setMaximumPoolSize(poolSize); @@ -70,7 +93,9 @@ public class H2Connector implements DatabaseConnector { try (Connection connection = getConnection()) { return callback.accept(connection); } catch (Exception ex) { - SongodaCore.getInstance().getLogger().severe("An error occurred executing a MySQL query: " + ex.getMessage()); + if (this.plugin != null) { + SongodaCore.getInstance().getLogger().severe("An error occurred executing a MySQL query: " + ex.getMessage()); + } ex.printStackTrace(); } return OptionalResult.empty(); @@ -81,7 +106,9 @@ public class H2Connector implements DatabaseConnector { try (Connection connection = getConnection()){ callback.accept(DSL.using(connection, SQLDialect.MYSQL)); } catch (Exception ex) { - this.plugin.getLogger().severe("An error occurred executing a MySQL query: " + ex.getMessage()); + if (this.plugin != null) { + this.plugin.getLogger().severe("An error occurred executing a MySQL query: " + ex.getMessage()); + } ex.printStackTrace(); } } @@ -91,7 +118,9 @@ public class H2Connector implements DatabaseConnector { try (Connection connection = getConnection()) { return callback.accept(DSL.using(connection, SQLDialect.MYSQL)); } catch (Exception ex) { - SongodaCore.getInstance().getLogger().severe("An error occurred executing a MySQL query: " + ex.getMessage()); + if (this.plugin != null) { + SongodaCore.getInstance().getLogger().severe("An error occurred executing a MySQL query: " + ex.getMessage()); + } ex.printStackTrace(); } return OptionalResult.empty(); diff --git a/Core/src/main/java/com/craftaro/core/database/SQLiteConnector.java b/Core/src/main/java/com/craftaro/core/database/SQLiteConnector.java index e7436ef2..2b02f4ce 100644 --- a/Core/src/main/java/com/craftaro/core/database/SQLiteConnector.java +++ b/Core/src/main/java/com/craftaro/core/database/SQLiteConnector.java @@ -1,6 +1,7 @@ package com.craftaro.core.database; import com.craftaro.core.SongodaCore; +import eu.decentsoftware.holograms.api.utils.scheduler.S; import org.bukkit.plugin.Plugin; import org.jooq.SQLDialect; import org.jooq.impl.DSL; @@ -16,6 +17,11 @@ public class SQLiteConnector implements DatabaseConnector { private final String connectionString; private Connection connection; + SQLiteConnector() { + this.plugin = null; + this.connectionString = "jdbc:sqlite:" + "."+File.separator+"db_test"+File.separator+"CraftaroCoreTestSQLite.db"; + } + public SQLiteConnector(Plugin plugin) { this.plugin = plugin; this.connectionString = "jdbc:sqlite:" + plugin.getDataFolder() + File.separator + plugin.getDescription().getName().toLowerCase() + ".db"; diff --git a/Core/src/test/java/com/craftaro/core/database/DatabaseTest.java b/Core/src/test/java/com/craftaro/core/database/DatabaseTest.java new file mode 100644 index 00000000..2a107ff5 --- /dev/null +++ b/Core/src/test/java/com/craftaro/core/database/DatabaseTest.java @@ -0,0 +1,409 @@ +package com.craftaro.core.database; + +import org.jooq.impl.DSL; +import org.jooq.impl.SQLDataType; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.UUID; + + +public class DatabaseTest { + + //Create database tests for DataManager + + static { + // Disable tips and logo for Jooq + System.setProperty("org.jooq.no-tips", "true"); + System.setProperty("org.jooq.no-logo", "true"); + } + + private static boolean deleteDirectory(File directoryToBeDeleted) { + File[] allContents = directoryToBeDeleted.listFiles(); + if (allContents != null) { + for (File file : allContents) { + deleteDirectory(file); + } + } + return directoryToBeDeleted.delete(); + } + + @Test + public void testId() { + File dbDir = new File("./db_test"); + File logsDir = new File("./logs"); + DataManager dataManager = new DataManager(); + if (!dataManager.getDatabaseConnector().isInitialized()) { + throw new RuntimeException("Database 'Data' test failed - DatabaseConnector not initialized"); + } + //Create tables + dataManager.getDatabaseConnector().connectDSL(context -> { + context.createTableIfNotExists("data_test") + .column("id", SQLDataType.INTEGER.identity(true)) + .column("name", SQLDataType.VARCHAR(16)) + .column("points", SQLDataType.INTEGER) + .column("other_points", SQLDataType.INTEGER) + .constraint(DSL.constraint().primaryKey(DSL.field("id"))) + .execute(); + }); + + int id = new Random().nextInt(1000); + DataTestId dataTest = new DataTestId(id, "Test", 10, 20); + dataManager.saveSync(dataTest); + + DataTestId dataTest2 = dataManager.load(id, DataTestId.class, "data_test"); + if (!dataTest.equals(dataTest2)) { + + + System.err.println("Database 'Data - ID' test failed"); + } else { + System.out.println("Database 'Data - ID' test passed"); + } + + dataManager.shutdownNow(); + + if (dbDir.exists()) { + deleteDirectory(dbDir); + } + if (logsDir.exists()) { + deleteDirectory(logsDir); + } + } + + @Test + public void testUUID() { + File dbDir = new File("./db_test"); + File logsDir = new File("./logs"); + + DataManager dataManager = new DataManager(); + if (!dataManager.getDatabaseConnector().isInitialized()) { + throw new RuntimeException("Database 'Data' test failed - DatabaseConnector not initialized"); + } + //Create tables + dataManager.getDatabaseConnector().connectDSL(context -> { + context.createTableIfNotExists("data_uuid_test") + .column("uuid", SQLDataType.VARCHAR(36)) + .column("name", SQLDataType.VARCHAR(16)) + .column("points", SQLDataType.INTEGER) + .column("other_points", SQLDataType.INTEGER) + .constraint(DSL.constraint().primaryKey(DSL.field("uuid"))) + .execute(); + }); + + + UUID uuid = UUID.randomUUID(); + DataTestUUID dataTest = new DataTestUUID(uuid, "Test", 10, 20); + dataManager.saveSync(dataTest); + + DataTestUUID dataTest2 = dataManager.load(uuid, DataTestUUID.class, "data_uuid_test"); + + if (!dataTest.equals(dataTest2)) { + System.err.println(dataTest); + System.err.println(dataTest2); + System.err.println("Database 'Data - UUID' test failed"); + } else { + System.out.println("Database 'Data - UUID' test passed"); + } + + + + dataManager.shutdownNow(); + + if (dbDir.exists()) { + deleteDirectory(dbDir); + } + if (logsDir.exists()) { + deleteDirectory(logsDir); + } + } + + @Test + public static void testConvert() { + File dbDir = new File("./db_test"); + File logsDir = new File("./logs"); + + DataManager sqlite = new DataManager(DatabaseType.SQLITE); + DataManager h2 = new DataManager(); + + if (!sqlite.getDatabaseConnector().isInitialized()) { + throw new RuntimeException("Database 'Data - Convert' test failed - DatabaseConnector not initialized"); + } + + if (!h2.getDatabaseConnector().isInitialized()) { + throw new RuntimeException("Database 'Data - Convert' test failed - DatabaseConnector not initialized"); + } + + //Create tables for SQLite + try (Connection sqliteConnection = sqlite.getDatabaseConnector().getConnection(); Connection h2Connection = h2.getDatabaseConnector().getConnection()) { + sqliteConnection.createStatement().execute("CREATE TABLE IF NOT EXISTS `data_convert_test` (`id` INTEGER PRIMARY KEY NOT NULL, `name` VARCHAR(16), `points` INTEGER, `other_points` INTEGER)"); + //create 2 other tables for some example data with different column names and types + sqliteConnection.createStatement().execute("CREATE TABLE IF NOT EXISTS `data_convert_test2` (`id` INTEGER PRIMARY KEY NOT NULL, `name` VARCHAR(16), `points` INTEGER, `other_points` INTEGER)"); + sqliteConnection.createStatement().execute("CREATE TABLE IF NOT EXISTS `data_convert_test3` (`id` INTEGER PRIMARY KEY NOT NULL, `name` VARCHAR(16), `points` INTEGER, `other_points` INTEGER)"); + + //Fill with some example data using loops + for (int i = 0; i < 10; i++) { + sqliteConnection.createStatement().execute("INSERT INTO `data_convert_test` (`name`, `points`, `other_points`) VALUES ('Test" + i + "', " + i + ", " + i + ")"); + sqliteConnection.createStatement().execute("INSERT INTO `data_convert_test2` (`name`, `points`, `other_points`) VALUES ('Test" + i + "', " + i + ", " + i + ")"); + sqliteConnection.createStatement().execute("INSERT INTO `data_convert_test3` (`name`, `points`, `other_points`) VALUES ('Test" + i + "', " + i + ", " + i + ")"); + } + + //Migrate tables and data to H2 + try { + // Export schema + DatabaseMetaData meta = sqliteConnection.getMetaData(); + ResultSet tables = meta.getTables(null, null, null, new String[]{"TABLE"}); + + while (tables.next()) { + String tableName = tables.getString("TABLE_NAME"); + Statement stmt = sqliteConnection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName); + + ResultSetMetaData metaRs = rs.getMetaData(); + int columnCount = metaRs.getColumnCount(); + + StringBuilder createTableQuery = new StringBuilder(); + createTableQuery.append("CREATE TABLE ").append(tableName).append(" ("); + + for (int i = 1; i <= columnCount; i++) { + String columnName = metaRs.getColumnName(i); + String columnType = metaRs.getColumnTypeName(i); + int columnSize = metaRs.getColumnDisplaySize(i); + + createTableQuery.append(columnName).append(" ").append(columnType).append("(").append(columnSize).append(")"); + + if (i < columnCount) { + createTableQuery.append(", "); + } + } + + createTableQuery.append(")"); + + h2Connection.createStatement().execute(createTableQuery.toString()); + + while (rs.next()) { + StringBuilder insertQuery = new StringBuilder(); + insertQuery.append("INSERT INTO ").append(tableName).append(" VALUES ("); + + for (int i = 1; i <= columnCount; i++) { + Object value = rs.getObject(i); + + if (value == null) { + insertQuery.append("NULL"); + } else if (value instanceof String || value instanceof Timestamp) { + insertQuery.append("'").append(value).append("'"); + } else { + insertQuery.append(value); + } + + if (i < columnCount) { + insertQuery.append(", "); + } + } + + insertQuery.append(")"); + h2Connection.createStatement().execute(insertQuery.toString()); + } + } + + //Query data from both databases and compare + Statement stmt = sqliteConnection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM `data_convert_test`"); + + //H2 + Statement stmt2 = h2Connection.createStatement(); + ResultSet rs2 = stmt2.executeQuery("SELECT * FROM `data_convert_test`"); + + //Compare data + while (rs.next() && rs2.next()) { + int id = rs.getInt("id"); + String name = rs.getString("name"); + int points = rs.getInt("points"); + int otherPoints = rs.getInt("other_points"); + + int id2 = rs2.getInt("id"); + String name2 = rs2.getString("name"); + int points2 = rs2.getInt("points"); + int otherPoints2 = rs2.getInt("other_points"); + + if (id != id2 || !name.equals(name2) || points != points2 || otherPoints != otherPoints2) { + System.err.println("Database 'Data - Convert' test failed - Data mismatch"); + System.err.println("SQLite: " + id + " " + name + " " + points + " " + otherPoints); + System.err.println("H2: " + id2 + " " + name2 + " " + points2 + " " + otherPoints2); + return; + } + } + System.out.println("Database 'Data - Convert' test passed"); + } catch (Exception e) { + e.printStackTrace(); + } + + } catch (SQLException e) { + throw new RuntimeException(e); + } + + + sqlite.shutdownNow(); + h2.shutdownNow(); + + if (dbDir.exists()) { + deleteDirectory(dbDir); + } + if (logsDir.exists()) { + deleteDirectory(logsDir); + } + } + + private static String getTableColumns(Connection sqliteConnection, String tableName) { + StringBuilder columns = new StringBuilder(); + try { + DatabaseMetaData meta = sqliteConnection.getMetaData(); + ResultSet rs = meta.getColumns(null, null, tableName, null); + + while (rs.next()) { + String columnName = rs.getString("COLUMN_NAME"); + String columnType = rs.getString("TYPE_NAME"); + + columns.append(columnName).append(" ").append(columnType).append(", "); + } + + rs.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + + columns.setLength(columns.length() - 2); + return columns.toString(); + } + + private static class DataTestId implements Data { + + private int id; + private String name; + private int points; + private int otherPoints; + + public DataTestId() { + } + + public DataTestId(int id, String name, int points, int otherPoints) { + this.id = id; + this.name = name; + this.points = points; + this.otherPoints = otherPoints; + } + + @Override + public int getId() { + return this.id; + } + + @Override + public Map serialize() { + Map map = new HashMap<>(); + map.put("id", this.id); + map.put("name", this.name); + map.put("points", this.points); + map.put("other_points", this.otherPoints); + return map; + } + + @Override + public Data deserialize(Map map) { + this.id = (int) map.get("id"); + this.name = (String) map.get("name"); + this.points = (int) map.get("points"); + this.otherPoints = (int) map.get("other_points"); + return this; + } + + @Override + public String getTableName() { + return "data_test"; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof DataTestId)) return false; + + DataTestId other = (DataTestId) obj; + return id == other.id && name.equals(other.name) && points == other.points && otherPoints == other.otherPoints; + } + } + + private static class DataTestUUID implements Data { + + private UUID uuid; + private String name; + private int points; + private int otherPoints; + + public DataTestUUID() { + } + + public DataTestUUID(UUID uuid, String name, int points, int otherPoints) { + this.uuid = uuid; + this.name = name; + this.points = points; + this.otherPoints = otherPoints; + } + + @Override + public UUID getUniqueId() { + return this.uuid; + } + + @Override + public Map serialize() { + Map map = new HashMap<>(); + map.put("uuid", this.uuid.toString()); + map.put("name", this.name); + map.put("points", this.points); + map.put("other_points", this.otherPoints); + return map; + } + + @Override + public Data deserialize(Map map) { + this.uuid = UUID.fromString((String) map.get("uuid")); + this.name = (String) map.get("name"); + this.points = (int) map.get("points"); + this.otherPoints = (int) map.get("other_points"); + return this; + } + + @Override + public String getTableName() { + return "data_uuid_test"; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof DataTestUUID)) return false; + + DataTestUUID other = (DataTestUUID) obj; + return uuid.equals(other.uuid) && name.equals(other.name) && points == other.points && otherPoints == other.otherPoints; + } + + @Override + public String toString() { + return "DataTestUUID{" + + "uuid=" + uuid + + ", name='" + name + '\'' + + ", points=" + points + + ", otherPoints=" + otherPoints + + '}'; + } + } +}