From 99526452d2dbba5d9b9f9f4ed21abba54a30390c Mon Sep 17 00:00:00 2001 From: CJ Burkey Date: Thu, 23 May 2024 00:37:54 -0400 Subject: [PATCH] Remove problematic Q2O calls :/ --- .../data/sqlite/SqLiteDataHandler.java | 2 +- .../sqlite/SqLiteTableMigrationManager.java | 30 +- .../claimchunk/data/sqlite/SqLiteWrapper.java | 452 +++++++++++------- .../cjburkey/claimchunk/TestSQLPlease.java | 53 ++ 4 files changed, 341 insertions(+), 196 deletions(-) create mode 100644 src/test/java/com/cjburkey/claimchunk/TestSQLPlease.java diff --git a/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqLiteDataHandler.java b/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqLiteDataHandler.java index 5dcca7b5..a9386043 100644 --- a/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqLiteDataHandler.java +++ b/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqLiteDataHandler.java @@ -52,7 +52,7 @@ public SqLiteDataHandler(@NotNull File claimChunkDb) { public void init() { joinedPlayers = new HashMap<>(); claimedChunks = new HashMap<>(); - sqLiteWrapper = new SqLiteWrapper(claimChunkDb); + sqLiteWrapper = new SqLiteWrapper(claimChunkDb, false); init = true; } diff --git a/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqLiteTableMigrationManager.java b/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqLiteTableMigrationManager.java index a62e111b..cf907c4d 100644 --- a/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqLiteTableMigrationManager.java +++ b/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqLiteTableMigrationManager.java @@ -1,11 +1,10 @@ package com.cjburkey.claimchunk.data.sqlite; import com.zaxxer.q2o.Q2Sql; +import com.zaxxer.q2o.SqlClosure; -import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; -import java.sql.SQLException; /** This class is responsible for creating, loading, and upgrading the database file. */ public class SqLiteTableMigrationManager { @@ -63,18 +62,21 @@ FOREIGN KEY(other_player_uuid) REFERENCES player_data(player_uuid) // Use this method to determine if a column exists in a table to perform migrations // TODO: MAYBE CHECK IF THIS WORKS @SuppressWarnings("unused") - private static boolean columnExists(Connection connection, String tableName, String columnName) - throws SQLException { - PreparedStatement statement = - connection.prepareCall( - """ - SELECT COUNT(*) FROM pragma_table_info(?) WHERE name=? - """); - statement.setString(1, tableName); - statement.setString(2, columnName); - ResultSet resultSet = statement.executeQuery(); - int count = resultSet.next() ? resultSet.getInt(1) : 0; - return count > 0; + public static boolean columnExists(String tableName, String columnName) { + return SqlClosure.sqlExecute( + connection -> { + try (PreparedStatement statement = + connection.prepareStatement( + """ + SELECT COUNT(*) FROM pragma_table_info(?) WHERE name=? + """)) { + statement.setString(1, tableName); + statement.setString(2, columnName); + ResultSet resultSet = statement.executeQuery(); + int count = resultSet.next() ? resultSet.getInt(1) : 0; + return count > 0; + } + }); } // Whenever a column is added or moved or transformed or whatever, add a diff --git a/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqLiteWrapper.java b/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqLiteWrapper.java index f8076aa6..e81c93fc 100644 --- a/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqLiteWrapper.java +++ b/src/main/java/com/cjburkey/claimchunk/data/sqlite/SqLiteWrapper.java @@ -1,6 +1,5 @@ package com.cjburkey.claimchunk.data.sqlite; -import com.cjburkey.claimchunk.Utils; import com.cjburkey.claimchunk.chunk.ChunkPlayerPermissions; import com.cjburkey.claimchunk.chunk.ChunkPos; import com.cjburkey.claimchunk.chunk.DataChunk; @@ -13,11 +12,17 @@ import java.io.Closeable; import java.io.File; import java.io.IOException; -import java.sql.*; -import java.util.*; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import java.util.regex.Pattern; import java.util.stream.Collectors; -public record SqLiteWrapper(File dbFile) implements Closeable { +public record SqLiteWrapper(File dbFile, boolean usesTransactionManager) implements Closeable { private static final String SELECT_CHUNK_ID_SQL = """ @@ -27,14 +32,16 @@ public record SqLiteWrapper(File dbFile) implements Closeable { WHERE chunk_world=? AND chunk_x=? AND chunk_z=? ) """; + private static final Pattern SELECT_CHUNK_ID_SQL_PATTERN = + Pattern.compile(Pattern.quote("%%SELECT_CHUNK_ID_SQL%%")); - public SqLiteWrapper(@NotNull File dbFile) { + public SqLiteWrapper(@NotNull File dbFile, boolean usesTransactionManager) { this.dbFile = dbFile; + this.usesTransactionManager = usesTransactionManager; try { - if (!dbFile.exists() && dbFile.createNewFile()) { - Utils.warn("Created empty database file"); - } + //noinspection ResultOfMethodCallIgnored + dbFile.createNewFile(); } catch (IOException e) { throw new RuntimeException( "Failed to create new database file even though it didn't exist!", e); @@ -44,31 +51,11 @@ public SqLiteWrapper(@NotNull File dbFile) { // for the DriverManager to search. SQLiteDataSource dataSource = new SQLiteDataSource(); dataSource.setUrl("jdbc:sqlite:" + dbFile); - //q2o.initializeTxSimple(dataSource); - q2o.initializeTxNone(dataSource); + if (usesTransactionManager) q2o.initializeTxSimple(dataSource); + else q2o.initializeTxNone(dataSource); // Initialize the tables and perform any changes to them SqLiteTableMigrationManager.go(); - - // TODO: WHY DOESN'T PARAMETER SUBSTITUTION WORK?!?!?! - // HELP ME PLEASE - Q2Sql.executeUpdate( - """ - INSERT INTO player_data ( - player_uuid, - last_ign, - last_online_time, - alerts_enabled, - extra_max_claims - ) VALUES ( - ?, - "CJBurkey", - 72468, - TRUE, - 0 - ) - """, - "8da30070-a1df-4e47-a913-f12424aabf6a"); } @Override @@ -79,152 +66,233 @@ public void close() { // -- DATABASE INTEGRATIONS! -- // public void addClaimedChunk(DataChunk chunk) { - Q2Obj.insert(new SqlDataChunk(chunk)); + SqlClosure.sqlExecute( + connection -> { + try (PreparedStatement statement = + connection.prepareStatement( + """ + INSERT INTO chunk_data ( + chunk_world, + chunk_x, + chunk_z, + owner_uuid + ) VALUES ( + ?, ?, ?, ? + ) + """)) { + int next = setChunkPosParams(statement, 1, chunk.chunk); + statement.setString(next, chunk.player.toString()); + statement.execute(); + return null; + } + }); } public void removeClaimedChunk(ChunkPos chunk) { - int chunkId = - SqlClosure.sqlExecute( - connection -> { - // Get chunk ID - ResultSet resultSet = - Q2Sql.executeQuery( - connection, + SqlClosure.sqlExecute( + connection -> { + // Remove all granted permissions for the chunk + try (PreparedStatement statement = + connection.prepareStatement( + replaceChunkIdQuery( """ - SELECT chunk_id FROM chunk_data - WHERE chunk_world=? AND chunk_x=? AND chunk_z=? - """, - chunk.world(), - chunk.x(), - chunk.z()); - return resultSet.next() ? resultSet.getInt(1) : -1; - }); - if (chunkId < 0) return; - - // Remove permissions - Q2Sql.executeUpdate( - """ - DELETE FROM chunk_permissions - WHERE chunk_id=? - """, - chunkId); - - // Remove chunks - Q2Sql.executeUpdate( - """ - DELETE FROM chunk_data - WHERE chunk_id=? - """, - chunkId); + DELETE FROM chunk_permissions + WHERE chunk_id=%%SELECT_CHUNK_ID_SQL%% + """))) { + setChunkPosParams(statement, 1, chunk); + statement.execute(); + } + + // Remove chunk + try (PreparedStatement statement = + connection.prepareStatement( + """ + DELETE FROM chunk_data + WHERE chunk_world=? AND chunk_x=? AND chunk_z=? + """)) { + setChunkPosParams(statement, 1, chunk); + statement.execute(); + } + + return null; + }); } public void addPlayer(FullPlayerData playerData) { - Q2Obj.insert(new SqlDataPlayer(playerData)); + SqlClosure.sqlExecute( + connection -> { + try (PreparedStatement statement = + connection.prepareStatement( + """ + INSERT INTO player_data ( + player_uuid, + last_ign, + chunk_name, + last_online_time, + alerts_enabled, + extra_max_claims + ) VALUES ( + ?, ?, ?, ?, ?, ? + ) + """)) { + statement.setString(1, playerData.player.toString()); + statement.setString(2, playerData.lastIgn); + statement.setString(3, playerData.chunkName); + statement.setLong(4, playerData.lastOnlineTime); + statement.setBoolean(5, playerData.alert); + statement.setInt(6, playerData.extraMaxClaims); + statement.execute(); + return null; + } + }); } public void setPlayerLastOnline(UUID player, long time) { - Q2Sql.executeUpdate( - """ - UPDATE player_data - SET last_online_time=? - WHERE player_uuid=? - """, - time, - player.toString()); + SqlClosure.sqlExecute( + connection -> { + try (PreparedStatement statement = + connection.prepareStatement( + """ + UPDATE player_data + SET last_online_time=? + WHERE player_uuid=? + """)) { + statement.setLong(1, time); + statement.setString(2, player.toString()); + statement.execute(); + return null; + } + }); } public void setPlayerChunkName(UUID player, String chunkName) { - Q2Sql.executeUpdate( - """ - UPDATE player_data - SET chunk_name=? - WHERE player_uuid=? - """, - chunkName, - player.toString()); + SqlClosure.sqlExecute( + connection -> { + try (PreparedStatement statement = + connection.prepareStatement( + """ + UPDATE player_data + SET chunk_name=? + WHERE player_uuid=? + """)) { + statement.setString(1, chunkName); + statement.setString(2, player.toString()); + statement.execute(); + return null; + } + }); } public void setPlayerReceiveAlerts(UUID player, boolean receiveAlerts) { - Q2Sql.executeUpdate( - """ - UPDATE player_data - SET receiveAlerts=? - WHERE player_uuid=? - """, - receiveAlerts, - player.toString()); + SqlClosure.sqlExecute( + connection -> { + try (PreparedStatement statement = + connection.prepareStatement( + """ + UPDATE player_data + SET receiveAlerts=? + WHERE player_uuid=? + """)) { + statement.setBoolean(1, receiveAlerts); + statement.setString(2, player.toString()); + statement.execute(); + return null; + } + }); } public void setPlayerExtraMaxClaims(UUID player, int extraMaxClaims) { - Q2Sql.executeUpdate( - """ - UPDATE player_data - SET extra_max_claims=? - WHERE player_uuid=? - """, - extraMaxClaims, - player.toString()); + SqlClosure.sqlExecute( + connection -> { + try (PreparedStatement statement = + connection.prepareStatement( + """ + UPDATE player_data + SET extra_max_claims=? + WHERE player_uuid=? + """)) { + statement.setInt(1, extraMaxClaims); + statement.setString(2, player.toString()); + statement.execute(); + return null; + } + }); } public void updateOrInsertPlayerAccess(ChunkPos chunk, UUID accessor, int permissionFlags) { // Check if the access already exists - // If so, update the permission bits - if (Q2Obj.countFromClause( - SqlDataChunkPermission.class, - "other_player_uuid=? AND chunk_id=" + SELECT_CHUNK_ID_SQL, - accessor.toString(), - chunk.world(), - chunk.x(), - chunk.z()) - > 0) { - Q2Sql.executeUpdate( - String.format( - """ - UPDATE chunk_permissions - SET permission_bits=? - WHERE chunk_id=%s - AND other_player_uuid=? - """, - SELECT_CHUNK_ID_SQL), - permissionFlags, - chunk.world(), - chunk.x(), - chunk.z(), - accessor.toString()); - } else { - Q2Sql.executeUpdate( - String.format( - """ - INSERT INTO chunk_permissions ( - chunk_id, - other_player_uuid, - permission_bits - ) VALUES ( - %s, ?, ? - ) - """, - SELECT_CHUNK_ID_SQL), - chunk.world(), - chunk.x(), - chunk.z(), - accessor.toString(), - permissionFlags); - } + SqlClosure.sqlExecute( + connection -> { + final boolean accessExists; + try (PreparedStatement statement = + connection.prepareStatement( + replaceChunkIdQuery( + """ +SELECT COUNT(*) FROM chunk_permissions +WHERE other_player_uuid=? AND chunk_id=%%SELECT_CHUNK_ID_SQL%% +"""))) { + statement.setString(1, accessor.toString()); + setChunkPosParams(statement, 2, chunk); + ResultSet resultSet = statement.executeQuery(); + accessExists = resultSet.next() && resultSet.getInt(1) > 0; + } + + // If the entry already exists, update the permission bits + if (accessExists) { + try (PreparedStatement statement = + connection.prepareStatement( + replaceChunkIdQuery( + """ + UPDATE chunk_permissions + SET permission_bits=? + WHERE chunk_id=%%SELECT_CHUNK_ID_SQL%% + AND other_player_uuid=? + """))) { + statement.setInt(1, permissionFlags); + int next = setChunkPosParams(statement, 2, chunk); + statement.setString(next, accessor.toString()); + statement.execute(); + } + } else { + try (PreparedStatement statement = + connection.prepareStatement( + replaceChunkIdQuery( + """ + INSERT INTO chunk_permissions ( + chunk_id, + other_player_uuid, + permission_bits + ) VALUES ( + %%SELECT_CHUNK_ID_SQL%%, ?, ? + ) + """))) { + int next = setChunkPosParams(statement, 1, chunk); + statement.setString(next, accessor.toString()); + statement.setInt(next + 1, permissionFlags); + statement.execute(); + } + } + + return null; + }); } public void removePlayerAccess(ChunkPos chunk, UUID accessor) { - Q2Sql.executeUpdate( - String.format( - """ - DELETE FROM chunk_permissions - WHERE chunk_id=%s, - AND other_player_uuid=? - """, - SELECT_CHUNK_ID_SQL), - chunk.world(), - chunk.x(), - chunk.z(), - accessor.toString()); + SqlClosure.sqlExecute( + connection -> { + try (PreparedStatement statement = + connection.prepareStatement( + replaceChunkIdQuery( + """ + DELETE FROM chunk_permissions + WHERE chunk_id=%%SELECT_CHUNK_ID_SQL%%, + AND other_player_uuid=? + """))) { + int next = setChunkPosParams(statement, 1, chunk); + statement.setString(next, accessor.toString()); + return null; + } + }); } // -- Loading stuff -- // @@ -239,37 +307,43 @@ public Collection getAllChunks() { HashMap> permissions = new HashMap<>(); HashMap owners = new HashMap<>(); - try (ResultSet resultSet = - SqlClosure.sqlExecute( - connection -> - Q2Sql.executeQuery( - connection, - """ - SELECT chunk_world, chunk_x, chunk_z, owner_uuid, - other_player_uuid, permission_bits - FROM chunk_permissions - RIGHT JOIN chunk_data - ON chunk_permissions.chunk_id=chunk_data.chunk_id - """))) { - while (resultSet.next()) { - String world = resultSet.getString("chunk_world"); - int chunk_x = resultSet.getInt("chunk_x"); - int chunk_z = resultSet.getInt("chunk_z"); - ChunkPos pos = new ChunkPos(world, chunk_x, chunk_z); - UUID owner = UUID.fromString(resultSet.getString("owner_uuid")); - UUID otherPlayer = UUID.fromString(resultSet.getString("other_player_uuid")); - ChunkPlayerPermissions chunkPerms = - new ChunkPlayerPermissions(resultSet.getInt("permission_bits")); - - permissions - .computeIfAbsent(pos, ignoredPos -> new HashMap<>()) - .put(otherPlayer, chunkPerms); - - owners.putIfAbsent(pos, owner); - } - } catch (Exception e) { - throw new RuntimeException("Failed to load chunk data!", e); - } + SqlClosure.sqlExecute( + connection -> { + try (PreparedStatement statement = + connection.prepareStatement( + """ + SELECT chunk_world, chunk_x, chunk_z, owner_uuid, + other_player_uuid, permission_bits + FROM chunk_permissions + RIGHT JOIN chunk_data + ON chunk_permissions.chunk_id=chunk_data.chunk_id + """)) { + ResultSet resultSet = statement.executeQuery(); + while (resultSet.next()) { + String world = resultSet.getString("chunk_world"); + int chunk_x = resultSet.getInt("chunk_x"); + int chunk_z = resultSet.getInt("chunk_z"); + ChunkPos pos = new ChunkPos(world, chunk_x, chunk_z); + + String otherUuid = resultSet.getString("other_player_uuid"); + if (otherUuid != null) { + UUID otherPlayer = + UUID.fromString(otherUuid); + ChunkPlayerPermissions chunkPerms = + new ChunkPlayerPermissions(resultSet.getInt("permission_bits")); + + permissions + .computeIfAbsent(pos, ignoredPos -> new HashMap<>()) + .put(otherPlayer, chunkPerms); + } + + UUID owner = UUID.fromString(resultSet.getString("owner_uuid")); + owners.putIfAbsent(pos, owner); + } + } + + return null; + }); for (SqlDataChunk chunk : Q2ObjList.fromClause(SqlDataChunk.class, null)) { owners.putIfAbsent( @@ -286,4 +360,20 @@ public Collection getAllChunks() { false)) .collect(Collectors.toList()); } + + // -- Queries -- // + + // Returns the index of the next parameter! + private int setChunkPosParams( + PreparedStatement statement, int worldParameterNum, ChunkPos chunkPos) + throws SQLException { + statement.setString(worldParameterNum, chunkPos.world()); + statement.setInt(worldParameterNum + 1, chunkPos.x()); + statement.setInt(worldParameterNum + 2, chunkPos.z()); + return worldParameterNum + 3; + } + + private String replaceChunkIdQuery(String sql) { + return SELECT_CHUNK_ID_SQL_PATTERN.matcher(sql).replaceAll(SELECT_CHUNK_ID_SQL); + } } diff --git a/src/test/java/com/cjburkey/claimchunk/TestSQLPlease.java b/src/test/java/com/cjburkey/claimchunk/TestSQLPlease.java new file mode 100644 index 00000000..758843e6 --- /dev/null +++ b/src/test/java/com/cjburkey/claimchunk/TestSQLPlease.java @@ -0,0 +1,53 @@ +package com.cjburkey.claimchunk; + +import com.cjburkey.claimchunk.chunk.ChunkPos; +import com.cjburkey.claimchunk.chunk.DataChunk; +import com.cjburkey.claimchunk.data.sqlite.SqLiteTableMigrationManager; +import com.cjburkey.claimchunk.data.sqlite.SqLiteWrapper; +import com.cjburkey.claimchunk.player.FullPlayerData; + +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.HashMap; +import java.util.UUID; + +class TestSQLPlease { + + @Test + void ensureSomeColumnsExistsAfterInitializing() { + File dbFile = randomDbFile(); + + try (SqLiteWrapper ignoredWrapper = new SqLiteWrapper(dbFile, false)) { + // Make sure that instantiating SqLiteWrapper created the tables + assert SqLiteTableMigrationManager.columnExists("player_data", "player_uuid"); + assert SqLiteTableMigrationManager.columnExists("chunk_data", "owner_uuid"); + assert SqLiteTableMigrationManager.columnExists("chunk_permissions", "permission_bits"); + assert !SqLiteTableMigrationManager.columnExists("chunk_hell", "permission_bits"); + assert !SqLiteTableMigrationManager.columnExists("player_data", "fake_col"); + + // Add a random player + UUID plyUuid = UUID.randomUUID(); + ignoredWrapper.addPlayer( + new FullPlayerData( + plyUuid, "SomeGuysName", null, System.currentTimeMillis(), true, 0)); + + // Add a chunk to the player + ChunkPos chunkPos = new ChunkPos("world", 10, -3); + ignoredWrapper.addClaimedChunk( + new DataChunk(chunkPos, plyUuid, new HashMap<>(), false)); + + // Make sure the chunk exists when we load from the database + assert ignoredWrapper.getAllChunks().stream() + .anyMatch( + chunk -> chunk.player.equals(plyUuid) && chunk.chunk.equals(chunkPos)); + } + + //noinspection ResultOfMethodCallIgnored + dbFile.delete(); + } + + protected static File randomDbFile() { + return new File(UUID.randomUUID() + ".tmp.sqlite3"); + } +}