diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/AdvancedCorePlugin.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/AdvancedCorePlugin.java index 1bed9a5caf..3b82d9b019 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/AdvancedCorePlugin.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/AdvancedCorePlugin.java @@ -1023,8 +1023,10 @@ public void updateReplacements() { * * @param storageType the storage type to load */ - @SuppressWarnings("deprecation") public void loadUserAPI(UserStorage storageType) { + if (storageType == null) { + throw new IllegalArgumentException("User storage must be SQLITE or MYSQL"); + } if (storageType.equals(UserStorage.SQLITE)) { ArrayList columns = new ArrayList<>(); Column key = new Column("uuid", DataType.STRING); @@ -1043,8 +1045,6 @@ public void loadUserAPI(UserStorage storageType) { getOptions().getYmlConfig().getData().getConfigurationSection("MySQL"))); } - } else if (storageType.equals(UserStorage.FLAT)) { - getLogger().severe("Detected using FLAT storage, this will be removed in the future!"); } } diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/UserData.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/UserData.java index f05924bd98..855da04a77 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/UserData.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/UserData.java @@ -3,14 +3,12 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.Map.Entry; - -import org.bukkit.configuration.file.FileConfiguration; import com.bencodez.advancedcore.api.user.usercache.UserDataCache; import com.bencodez.advancedcore.api.user.usercache.change.UserDataChangeInt; import com.bencodez.advancedcore.api.user.usercache.change.UserDataChangeString; -import com.bencodez.advancedcore.thread.FileThread; +import com.bencodez.advancedcore.bukkit.user.storage.BukkitSqlUserStorage; +import com.bencodez.advancedcore.core.user.storage.SqlUserDataAccess; import com.bencodez.simpleapi.array.ArrayUtils; import com.bencodez.simpleapi.sql.Column; import com.bencodez.simpleapi.sql.data.DataValue; @@ -27,6 +25,17 @@ public class UserData { private AdvancedCoreUser user; + private final BukkitSqlUserStorage sqlStorage = new BukkitSqlUserStorage( + () -> user.getPlugin(), () -> user.getUUID()); + private final SqlUserDataAccess sqlData = new SqlUserDataAccess(sqlStorage, this::getStorageRow); + + private List getStorageRow(UserStorage storage) { + if (storage.equals(UserStorage.MYSQL)) { + return getMySqlRow(); + } + return getSQLiteRow(); + } + public UserData(AdvancedCoreUser user) { this.user = user; } @@ -39,14 +48,7 @@ public void clearTempCache() { } public HashMap convert(List cols) { - HashMap data = new HashMap<>(); - if (cols != null) { - for (Column col : cols) { - data.put(col.getName(), col.getValue()); - } - } - - return data; + return SqlUserDataAccess.convert(cols); } public boolean getBoolean(String key) { @@ -65,11 +67,6 @@ public boolean getBoolean(String key, boolean useCache, boolean waitForCache) { return getBoolean(key, UserDataFetchMode.fromBooleans(useCache, waitForCache)); } - @Deprecated - public FileConfiguration getData(String uuid) { - return FileThread.getInstance().getThread().getData(this, uuid); - } - public DataValue getDataValue(String key) { boolean isInt = user.getPlugin().getUserManager().getDataManager().isInt(key); if (isInt) { @@ -127,15 +124,8 @@ public int getInt(String key, int def, boolean useCache, boolean waitForCache) { UserDataFetchMode.fromBooleans(useCache, waitForCache)); } - @SuppressWarnings("deprecation") public int getInt(UserStorage storage, String key, int def, UserDataFetchMode mode) { if (key == null || key.isEmpty()) { - if (storage.equals(UserStorage.FLAT)) { - try { - return getData(user.getUUID()).getInt(key, def); - } catch (Exception ignored) { - } - } return def; } @@ -197,58 +187,7 @@ public int getInt(UserStorage storage, String key, int def, UserDataFetchMode mo } // 3) Storage lookup - if (storage.equals(UserStorage.SQLITE)) { - List row = getSQLiteRow(); - if (row != null) { - for (Column element : row) { - if (element.getName().equals(key)) { - DataValue value = element.getValue(); - if (value.isInt()) { - return value.getInt(); - } - if (value.isString()) { - String str = value.getString(); - if (str != null) { - try { - return Integer.parseInt(str); - } catch (Exception ignored) { - } - } - return def; - } - } - } - } - } else if (storage.equals(UserStorage.MYSQL)) { - List row = getMySqlRow(); - if (row != null) { - for (Column element : row) { - if (element.getName().equals(key)) { - DataValue value = element.getValue(); - if (value.isInt()) { - return value.getInt(); - } - if (value.isString()) { - String str = value.getString(); - if (str != null) { - try { - return Integer.parseInt(str); - } catch (Exception ignored) { - } - } - return def; - } - } - } - } - } else if (storage.equals(UserStorage.FLAT)) { - try { - return getData(user.getUUID()).getInt(key, def); - } catch (Exception ignored) { - } - } - - return def; + return sqlData.getInt(storage, key, def); } /** @@ -263,28 +202,8 @@ public ArrayList getKeys() { return getKeys(user.getPlugin().getStorageType()); } - @SuppressWarnings("deprecation") public ArrayList getKeys(UserStorage storage) { - ArrayList keys = new ArrayList<>(); - if (storage.equals(UserStorage.FLAT)) { - keys = new ArrayList<>(getData(user.getUUID()).getConfigurationSection("").getKeys(false)); - } else if (storage.equals(UserStorage.MYSQL)) { - List col = getMySqlRow(); - if (col != null && !col.isEmpty()) { - for (Column c : col) { - keys.add(c.getName()); - } - } - } else if (storage.equals(UserStorage.SQLITE)) { - List col = getSQLiteRow(); - if (col != null && !col.isEmpty()) { - for (Column c : col) { - keys.add(c.getName()); - } - } - } - - return keys; + return sqlData.getKeys(storage); } /** @@ -304,11 +223,11 @@ public ArrayList getKeys(UserStorage storage, boolean waitForCache) { } public List getMySqlRow() { - return user.getPlugin().getMysql().getExact(user.getUUID()); + return sqlStorage.readRow(UserStorage.MYSQL); } public List getSQLiteRow() { - return user.getPlugin().getSQLiteUserTable().getExact(new Column("uuid", new DataValueString(user.getUUID()))); + return sqlStorage.readRow(UserStorage.SQLITE); } public String getString(String key) { @@ -319,7 +238,6 @@ public String getString(String key, UserDataFetchMode mode) { return getString(user.getPlugin().getStorageType(), key, mode); } - @SuppressWarnings("deprecation") public String getString(UserStorage storage, String key, UserDataFetchMode mode) { if (key == null || key.isEmpty()) { return ""; @@ -366,36 +284,7 @@ public String getString(UserStorage storage, String key, UserDataFetchMode mode) } // 3) Storage lookup - if (storage.equals(UserStorage.SQLITE)) { - List row = getSQLiteRow(); - if (row != null) { - for (Column element : row) { - if (element.getName().equals(key) - && (element.getValue().isString() || element.getValue().isBoolean())) { - String st = element.getValue().getString(); - return (st != null && !st.equalsIgnoreCase("null")) ? st : ""; - } - } - } - } else if (storage.equals(UserStorage.MYSQL)) { - List row = getMySqlRow(); - if (row != null) { - for (Column element : row) { - if (element.getName().equals(key) - && (element.getValue().isString() || element.getValue().isBoolean())) { - String st = element.getValue().getString(); - return (st != null && !st.equalsIgnoreCase("null")) ? st : ""; - } - } - } - } else if (storage.equals(UserStorage.FLAT)) { - try { - return getData(user.getUUID()).getString(key, ""); - } catch (Exception ignored) { - } - } - - return ""; + return sqlData.getString(storage, key); } /** @@ -454,50 +343,16 @@ public HashMap getValues() { return getValues(user.getPlugin().getStorageType()); } - @SuppressWarnings("deprecation") public HashMap getValues(UserStorage storage) { - if (storage.equals(UserStorage.MYSQL)) { - return convert(getMySqlRow()); - } - if (storage.equals(UserStorage.SQLITE)) { - return convert(getSQLiteRow()); - } else if (storage.equals(UserStorage.FLAT)) { - HashMap list = new HashMap<>(); - FileConfiguration data = getData(user.getUUID()); - for (String str : data.getKeys(false)) { - if (data.isInt(str)) { - list.put(str, new DataValueInt(data.getInt(str))); - } else { - list.put(str, new DataValueString(data.getString(str, ""))); - } - } - return list; - } - return null; + return convert(sqlData.readRow(storage)); } - @SuppressWarnings("deprecation") public boolean hasData() { - if (user.getPlugin().getStorageType().equals(UserStorage.MYSQL)) { - return user.getPlugin().getMysql().containsKey(user.getUUID()); - } - if (user.getPlugin().getStorageType().equals(UserStorage.SQLITE)) { - return user.getPlugin().getSQLiteUserTable().containsKey(user.getUUID()); - } else if (user.getPlugin().getStorageType().equals(UserStorage.FLAT)) { - return FileThread.getInstance().getThread().hasPlayerFile(user.getUUID()); - } - return false; + return sqlData.hasData(user.getPlugin().getStorageType()); } - @SuppressWarnings("deprecation") public void remove() { - if (user.getPlugin().getStorageType().equals(UserStorage.MYSQL)) { - user.getPlugin().getMysql().deletePlayer(user.getUUID()); - } else if (user.getPlugin().getStorageType().equals(UserStorage.SQLITE)) { - user.getPlugin().getSQLiteUserTable().delete(new Column("uuid", new DataValueString(user.getUUID()))); - } else if (user.getPlugin().getStorageType().equals(UserStorage.FLAT)) { - FileThread.getInstance().getThread().deletePlayerFile(user.getUUID()); - } + sqlData.remove(user.getPlugin().getStorageType()); user.clearCache(); } @@ -509,11 +364,6 @@ public void setBoolean(String key, boolean value, boolean queue) { setString(key, "" + value, queue); } - @Deprecated - private void setData(final String uuid, final String path, final Object value) { - FileThread.getInstance().getThread().setData(this, uuid, path, value); - } - public void setInt(final String key, final int value) { setInt(key, value, true); } @@ -530,7 +380,6 @@ public void setInt(UserStorage storage, final String key, final int value, boole setInt(storage, key, value, queue, false); } - @SuppressWarnings("deprecation") public void setInt(final UserStorage storage, final String key, final int value, boolean queue, boolean async) { if (key.equals("")) { user.getPlugin().debug("No key: " + key + " to " + value); @@ -556,18 +405,7 @@ public void setInt(final UserStorage storage, final String key, final int value, @Override public void run() { - if (storage.equals(UserStorage.SQLITE)) { - ArrayList columns = new ArrayList<>(); - Column primary = new Column("uuid", new DataValueString(user.getUUID())); - Column column = new Column(key, new DataValueInt(value)); - columns.add(primary); - columns.add(column); - user.getPlugin().getSQLiteUserTable().update(primary, columns); - } else if (storage.equals(UserStorage.MYSQL)) { - user.getPlugin().getMysql().update(user.getUUID(), key, new DataValueInt(value)); - } else if (storage.equals(UserStorage.FLAT)) { - setData(user.getUUID(), key, value); - } + sqlData.setInt(storage, key, value); if (!user.isCached()) { user.getPlugin().getUserManager().onChange(user, key); @@ -576,18 +414,7 @@ public void run() { }); } else { // process change right away - if (storage.equals(UserStorage.SQLITE)) { - ArrayList columns = new ArrayList<>(); - Column primary = new Column("uuid", new DataValueString(user.getUUID())); - Column column = new Column(key, new DataValueInt(value)); - columns.add(primary); - columns.add(column); - user.getPlugin().getSQLiteUserTable().update(primary, columns); - } else if (storage.equals(UserStorage.MYSQL)) { - user.getPlugin().getMysql().update(user.getUUID(), key, new DataValueInt(value)); - } else if (storage.equals(UserStorage.FLAT)) { - setData(user.getUUID(), key, value); - } + sqlData.setInt(storage, key, value); if (!user.isCached()) { user.getPlugin().getUserManager().onChange(user, key); @@ -611,7 +438,6 @@ public void setString(UserStorage storage, final String key, final String value, setString(storage, key, value, queue, false); } - @SuppressWarnings("deprecation") public void setString(final UserStorage storage, final String key, final String value, boolean queue, boolean async) { if (key.equals("") && value != null) { @@ -638,36 +464,14 @@ public void setString(final UserStorage storage, final String key, final String @Override public void run() { - if (storage.equals(UserStorage.SQLITE)) { - ArrayList columns = new ArrayList<>(); - Column primary = new Column("uuid", new DataValueString(user.getUUID())); - Column column = new Column(key, new DataValueString(value)); - columns.add(primary); - columns.add(column); - user.getPlugin().getSQLiteUserTable().update(primary, columns); - } else if (storage.equals(UserStorage.MYSQL)) { - user.getPlugin().getMysql().update(user.getUUID(), key, new DataValueString(value)); - } else if (storage.equals(UserStorage.FLAT)) { - setData(user.getUUID(), key, value); - } + sqlData.setString(storage, key, value); if (!user.isCached()) { user.getPlugin().getUserManager().onChange(user, key); } } }); } else { - if (storage.equals(UserStorage.SQLITE)) { - ArrayList columns = new ArrayList<>(); - Column primary = new Column("uuid", new DataValueString(user.getUUID())); - Column column = new Column(key, new DataValueString(value)); - columns.add(primary); - columns.add(column); - user.getPlugin().getSQLiteUserTable().update(primary, columns); - } else if (storage.equals(UserStorage.MYSQL)) { - user.getPlugin().getMysql().update(user.getUUID(), key, new DataValueString(value)); - } else if (storage.equals(UserStorage.FLAT)) { - setData(user.getUUID(), key, value); - } + sqlData.setString(storage, key, value); if (!user.isCached()) { user.getPlugin().getUserManager().onChange(user, key); } @@ -700,36 +504,8 @@ public void setValues(String key, DataValue value) { setValues(user.getPlugin().getStorageType(), values); } - @SuppressWarnings("deprecation") public void setValues(UserStorage storage, HashMap values) { - if (storage.equals(UserStorage.MYSQL)) { - if (user.getPlugin().getMysql() != null) { - ArrayList cols = new ArrayList<>(); - for (Entry entry : values.entrySet()) { - if (!entry.getKey().equals("uuid")) { - cols.add(new Column(entry.getKey(), entry.getValue())); - } - } - user.getPlugin().getMysql().update(user.getUUID(), cols, false); - } - } else if (storage.equals(UserStorage.SQLITE)) { - ArrayList cols = new ArrayList<>(); - for (Entry entry : values.entrySet()) { - if (!entry.getKey().equals("uuid")) { - cols.add(new Column(entry.getKey(), entry.getValue())); - } - user.getPlugin().getSQLiteUserTable().update(new Column("uuid", new DataValueString(user.getUUID())), - cols); - } - } else if (storage.equals(UserStorage.FLAT)) { - for (Entry entry : values.entrySet()) { - if (entry.getValue() instanceof DataValueString) { - setData(user.getUUID(), entry.getKey(), entry.getValue().getString()); - } else if (entry.getValue() instanceof DataValueInt) { - setData(user.getUUID(), entry.getKey(), entry.getValue().getInt()); - } - } - } + sqlData.setValues(storage, values); } public void tempCache() { diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/UserManager.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/UserManager.java index edb608a9a9..28007d0809 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/UserManager.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/UserManager.java @@ -1,6 +1,5 @@ package com.bencodez.advancedcore.api.user; -import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -20,7 +19,6 @@ import com.bencodez.simpleapi.array.ArrayUtils; import com.bencodez.simpleapi.sql.Column; import com.bencodez.simpleapi.sql.DataType; -import com.bencodez.simpleapi.sql.data.DataValue; import com.bencodez.simpleapi.sql.data.DataValueString; import lombok.Getter; @@ -49,23 +47,14 @@ public UserManager(AdvancedCorePlugin plugin) { load(); } - @SuppressWarnings("deprecation") public void copyColumnData(String columnFromName, String columnToName) { if (plugin.getStorageType().equals(UserStorage.MYSQL)) { plugin.getMysql().copyColumnData(columnFromName, columnToName, DataType.STRING); } else if (plugin.getStorageType().equals(UserStorage.SQLITE)) { plugin.getSQLiteUserTable().copyColumnData(columnFromName, columnToName, DataType.STRING); - } else if (plugin.getStorageType().equals(UserStorage.FLAT)) { - for (String uuid : getAllUUIDs()) { - AdvancedCoreUser user = getUser(UUID.fromString(uuid)); - user.userDataFetechMode(UserDataFetchMode.NO_CACHE); - DataValue data = user.getData().getDataValue(columnFromName); - user.getData().setValues(columnToName, data); - } } } - @SuppressWarnings("deprecation") public List getAllColumns() { UserStorage storage = plugin.getStorageType(); if (storage.equals(UserStorage.SQLITE)) { @@ -73,14 +62,8 @@ public List getAllColumns() { } if (storage.equals(UserStorage.MYSQL)) { return plugin.getMysql().getColumns(); - } else { - AdvancedCoreUser user = getRandomUser(); - if (user == null) { - return new ArrayList<>(); - } - user.userDataFetechMode(UserDataFetchMode.NO_CACHE); - return new ArrayList<>(user.getData().getData(user.getUUID()).getConfigurationSection("").getKeys(false)); } + return new ArrayList<>(); } @Deprecated @@ -94,34 +77,14 @@ public HashMap> getAllKeys(UserStorage storage) { } if (storage.equals(UserStorage.MYSQL)) { return plugin.getMysql().getAllQuery(); - } else { - HashMap> cols = new HashMap<>(); - for (String uuid : getAllUUIDs()) { - AdvancedCoreUser user = getUser(UUID.fromString(uuid)); - user.userDataFetechMode(UserDataFetchMode.NO_CACHE); - ArrayList col = new ArrayList<>(); - for (Entry entry : user.getData().getValues().entrySet()) { - col.add(new Column(entry.getKey(), entry.getValue())); - } - cols.put(UUID.fromString(uuid), col); - } - return cols; } + return new HashMap<>(); } - @SuppressWarnings("deprecation") public ArrayList getAllPlayerNames() { if (plugin.isLoadUserData()) { ArrayList names = new ArrayList<>(); - if (AdvancedCorePlugin.getInstance().getStorageType().equals(UserStorage.FLAT)) { - for (String uuid : getAllUUIDs()) { - AdvancedCoreUser user = getUser(UUID.fromString(uuid)); - String name = user.getPlayerName(); - if (name != null && !name.isEmpty() && !name.equalsIgnoreCase("Error getting name")) { - names.add(name); - } - } - } else if (AdvancedCorePlugin.getInstance().getStorageType().equals(UserStorage.SQLITE)) { + if (AdvancedCorePlugin.getInstance().getStorageType().equals(UserStorage.SQLITE)) { ArrayList data = plugin.getSQLiteUserTable().getNames(); for (String name : data) { if (name != null && !name.isEmpty() && !name.equalsIgnoreCase("Error getting name")) { @@ -145,13 +108,11 @@ public ArrayList getAllPlayerNames() { * Storage-agnostic streaming iteration over all users + their column data. * * MYSQL: uses plugin.getMysql().forEachUser(...) SQLITE: uses - * plugin.getSQLiteUserTable().forEachUser(...) FLAT: iterates UUID files and - * builds columns per user (no giant map). + * plugin.getSQLiteUserTable().forEachUser(...). * * @param perUser BiConsumer called per user with UUID and column list * @param onFinished Consumer called once after all users processed with total */ - @SuppressWarnings("deprecation") public void forEachUserKeys(BiConsumer> perUser, Consumer onFinished) { UserStorage storage = plugin.getStorageType(); @@ -173,60 +134,15 @@ public void forEachUserKeys(BiConsumer> perUser, Consume return; } - // FLAT fallback (stream-like; no giant HashMap) - int processed = 0; - try { - for (String uuidStr : getAllUUIDs(UserStorage.FLAT)) { - if (uuidStr == null || uuidStr.isEmpty() || "null".equalsIgnoreCase(uuidStr)) { - continue; - } - - UUID uuid; - try { - uuid = UUID.fromString(uuidStr); - } catch (IllegalArgumentException ignored) { - continue; - } - - AdvancedCoreUser user = getUser(uuid); - user.userDataFetechMode(UserDataFetchMode.NO_CACHE); - - ArrayList colList = new ArrayList<>(); - for (Entry entry : user.getData().getValues().entrySet()) { - colList.add(new Column(entry.getKey(), entry.getValue())); - } - - processed++; - perUser.accept(uuid, colList); - } - } finally { - if (onFinished != null) { - onFinished.accept(processed); - } - } + throw new IllegalStateException("User storage is not configured"); } public ArrayList getAllUUIDs() { return ArrayUtils.removeDuplicates(getAllUUIDs(plugin.getStorageType())); } - @SuppressWarnings("deprecation") public ArrayList getAllUUIDs(UserStorage storage) { if (plugin.isLoadUserData()) { - if (storage.equals(UserStorage.FLAT)) { - File folder = new File(plugin.getDataFolder() + File.separator + "Data"); - String[] fileNames = folder.list(); - ArrayList uuids = new ArrayList<>(); - if (fileNames != null) { - for (String playerFile : fileNames) { - if (!playerFile.equals("null") && !playerFile.equals("")) { - String uuid = playerFile.replace(".yml", ""); - uuids.add(uuid); - } - } - } - return uuids; - } if (storage.equals(UserStorage.SQLITE)) { List cols = plugin.getSQLiteUserTable().getRows(); ArrayList uuids = new ArrayList<>(); @@ -253,22 +169,12 @@ public ArrayList getAllUUIDs(UserStorage storage) { return new ArrayList<>(); } - @SuppressWarnings("deprecation") public ArrayList getNumbersInColumn(String columnName) { if (plugin.getStorageType().equals(UserStorage.MYSQL)) { return plugin.getMysql().getNumbersInColumn(columnName); } if (plugin.getStorageType().equals(UserStorage.SQLITE)) { return plugin.getSQLiteUserTable().getNumbersInColumn(columnName); - } else if (plugin.getStorageType().equals(UserStorage.FLAT)) { - ArrayList nums = new ArrayList<>(); - for (String uuid : getAllUUIDs()) { - AdvancedCoreUser user = getUser(UUID.fromString(uuid)); - user.userDataFetechMode(UserDataFetchMode.NO_CACHE); - int num = user.getData().getInt(columnName, 0, true, true); - nums.add(num); - } - return nums; } return new ArrayList<>(); } @@ -462,21 +368,6 @@ public void removeAllKeyValues(String key, DataType type) { plugin.getSQLiteUserTable().wipeColumnData(key, type); } else if (plugin.getStorageType().equals(UserStorage.MYSQL)) { plugin.getMysql().wipeColumnData(key, type); - } else { - for (String uuid : getAllUUIDs()) { - AdvancedCoreUser user = getUser(UUID.fromString(uuid)); - user.userDataFetechMode(UserDataFetchMode.NO_CACHE); - switch (type) { - case INTEGER: - user.getData().setInt(key, 0); - break; - case STRING: - user.getData().setString(key, ""); - break; - default: - break; - } - } } } diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/UserStorage.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/UserStorage.java index 6e062a37f8..aae6e9ea0f 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/UserStorage.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/UserStorage.java @@ -2,13 +2,13 @@ public enum UserStorage { - MYSQL, SQLITE, @Deprecated - FLAT; + MYSQL, SQLITE; public static UserStorage value(String str) { - // if (str.equalsIgnoreCase("FLAT")) { - // return SQLITE; - // } + if (str != null && "FLAT".equalsIgnoreCase(str.trim())) { + throw new IllegalArgumentException("FLAT user storage has been removed. Convert existing data to " + + "SQLITE or MYSQL using the previous version before upgrading; existing Data files are not modified."); + } for (UserStorage s : values()) { if (s.toString().equalsIgnoreCase(str)) { return s; diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/bukkit/user/storage/BukkitSqlUserStorage.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/bukkit/user/storage/BukkitSqlUserStorage.java new file mode 100644 index 0000000000..6ffe2b56ed --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/bukkit/user/storage/BukkitSqlUserStorage.java @@ -0,0 +1,102 @@ +package com.bencodez.advancedcore.bukkit.user.storage; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.function.Supplier; + +import com.bencodez.advancedcore.AdvancedCorePlugin; +import com.bencodez.advancedcore.api.user.UserStorage; +import com.bencodez.advancedcore.core.user.storage.SqlUserStorage; +import com.bencodez.simpleapi.sql.Column; +import com.bencodez.simpleapi.sql.data.DataValue; +import com.bencodez.simpleapi.sql.data.DataValueString; + +/** + * Adapts the existing SQL providers. Plugin/table/UUID lookup stays live so a + * queued operation resolves the same current ownership as the legacy facade. + * Construction creates no user, cache, connection, executor, or schema. + */ +public final class BukkitSqlUserStorage implements SqlUserStorage { + private final Supplier plugin; + private final Supplier uuid; + + public BukkitSqlUserStorage(Supplier plugin, Supplier uuid) { + this.plugin = Objects.requireNonNull(plugin, "plugin"); + this.uuid = Objects.requireNonNull(uuid, "uuid"); + } + + private AdvancedCorePlugin owner() { + return Objects.requireNonNull(plugin.get(), "plugin is not initialized"); + } + + private Column primary() { + return new Column("uuid", new DataValueString(uuid.get())); + } + + @Override + public List readRow(UserStorage storage) { + if (Objects.requireNonNull(storage, "storage") == UserStorage.MYSQL) { + return owner().getMysql().getExact(uuid.get()); + } + return owner().getSQLiteUserTable().getExact(primary()); + } + + @Override + public boolean contains(UserStorage storage) { + if (Objects.requireNonNull(storage, "storage") == UserStorage.MYSQL) { + return owner().getMysql().containsKey(uuid.get()); + } + return owner().getSQLiteUserTable().containsKey(uuid.get()); + } + + @Override + public void delete(UserStorage storage) { + if (Objects.requireNonNull(storage, "storage") == UserStorage.MYSQL) { + owner().getMysql().deletePlayer(uuid.get()); + } else { + owner().getSQLiteUserTable().delete(primary()); + } + } + + @Override + public void write(UserStorage storage, String key, DataValue value) { + if (Objects.requireNonNull(storage, "storage") == UserStorage.SQLITE) { + ArrayList columns = new ArrayList<>(); + Column primary = primary(); + columns.add(primary); + columns.add(new Column(key, value)); + owner().getSQLiteUserTable().update(primary, columns); + } else { + owner().getMysql().update(uuid.get(), key, value); + } + } + + @Override + public void writeValues(UserStorage storage, HashMap values) { + if (Objects.requireNonNull(storage, "storage") == UserStorage.MYSQL) { + // Preserve the existing bulk-write no-op while MySQL is unavailable. + if (owner().getMysql() != null) { + ArrayList columns = new ArrayList<>(); + for (Entry entry : values.entrySet()) { + if (!entry.getKey().equals("uuid")) { + columns.add(new Column(entry.getKey(), entry.getValue())); + } + } + owner().getMysql().update(uuid.get(), columns, false); + } + } else { + ArrayList columns = new ArrayList<>(); + for (Entry entry : values.entrySet()) { + if (!entry.getKey().equals("uuid")) { + columns.add(new Column(entry.getKey(), entry.getValue())); + } + // Deliberately preserve legacy cumulative per-entry updates. + // Moving this call outside the loop would change write/failure ordering. + owner().getSQLiteUserTable().update(primary(), columns); + } + } + } +} diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/user/storage/SqlUserDataAccess.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/user/storage/SqlUserDataAccess.java new file mode 100644 index 0000000000..c5daa272ad --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/user/storage/SqlUserDataAccess.java @@ -0,0 +1,121 @@ +package com.bencodez.advancedcore.core.user.storage; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +import com.bencodez.advancedcore.api.user.UserStorage; +import com.bencodez.simpleapi.sql.Column; +import com.bencodez.simpleapi.sql.data.DataValue; +import com.bencodez.simpleapi.sql.data.DataValueInt; +import com.bencodez.simpleapi.sql.data.DataValueString; + +/** + * Shared SQL row interpretation and user-data access, without a Bukkit plugin. + * Cache selection and asynchronous write ordering remain with the caller. + */ +public final class SqlUserDataAccess { + private final SqlUserStorage storage; + private final Function> rows; + + public SqlUserDataAccess(SqlUserStorage storage) { + this(storage, Objects.requireNonNull(storage, "storage")::readRow); + } + + /** + * A separate live row reader preserves existing facade overrides. Neither + * callback is invoked during construction, and returned rows are not cached. + */ + public SqlUserDataAccess(SqlUserStorage storage, Function> rows) { + this.storage = Objects.requireNonNull(storage, "storage"); + this.rows = Objects.requireNonNull(rows, "rows"); + } + + public List readRow(UserStorage backend) { + return rows.apply(Objects.requireNonNull(backend, "backend")); + } + + public int getInt(UserStorage backend, String key, int def) { + if (key == null || key.isEmpty()) return def; + List row = readRow(backend); + if (row != null) { + for (Column element : row) { + if (element.getName().equals(key)) { + DataValue value = element.getValue(); + if (value.isInt()) return value.getInt(); + if (value.isString()) { + String str = value.getString(); + if (str != null) { + try { + return Integer.parseInt(str); + } catch (Exception ignored) { + // Preserve the legacy invalid-string default, including duplicates. + } + } + return def; + } + } + } + } + return def; + } + + public String getString(UserStorage backend, String key) { + if (key == null || key.isEmpty()) return ""; + List row = readRow(backend); + if (row != null) { + for (Column element : row) { + if (element.getName().equals(key) + && (element.getValue().isString() || element.getValue().isBoolean())) { + String value = element.getValue().getString(); + return value != null && !value.equalsIgnoreCase("null") ? value : ""; + } + } + } + return ""; + } + + public ArrayList getKeys(UserStorage backend) { + ArrayList keys = new ArrayList<>(); + List row = readRow(backend); + if (row != null && !row.isEmpty()) { + for (Column column : row) keys.add(column.getName()); + } + return keys; + } + + /** Mutable map with the original value objects; duplicate names are last-wins. */ + public static HashMap convert(List columns) { + HashMap values = new HashMap<>(); + if (columns != null) { + for (Column column : columns) values.put(column.getName(), column.getValue()); + } + return values; + } + + public HashMap getValues(UserStorage backend) { + return convert(readRow(backend)); + } + + public boolean hasData(UserStorage backend) { + return storage.contains(Objects.requireNonNull(backend, "backend")); + } + + public void remove(UserStorage backend) { + storage.delete(Objects.requireNonNull(backend, "backend")); + } + + public void setInt(UserStorage backend, String key, int value) { + storage.write(Objects.requireNonNull(backend, "backend"), key, new DataValueInt(value)); + } + + public void setString(UserStorage backend, String key, String value) { + storage.write(Objects.requireNonNull(backend, "backend"), key, new DataValueString(value)); + } + + public void setValues(UserStorage backend, HashMap values) { + storage.writeValues(Objects.requireNonNull(backend, "backend"), values); + } +} diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/user/storage/SqlUserStorage.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/user/storage/SqlUserStorage.java new file mode 100644 index 0000000000..b7bca5caf7 --- /dev/null +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/core/user/storage/SqlUserStorage.java @@ -0,0 +1,30 @@ +package com.bencodez.advancedcore.core.user.storage; + +import java.util.HashMap; +import java.util.List; + +import com.bencodez.advancedcore.api.user.UserStorage; +import com.bencodez.simpleapi.sql.Column; +import com.bencodez.simpleapi.sql.data.DataValue; + +/** + * SQL operations for one caller-owned user identity. No game API, user cache, + * executor, connection pool, or lifecycle is owned here. A platform supplies its + * existing backend implementations. Calls may block; use the owner's established + * storage execution context, never a game/region thread for database work. + * + * These operations retain the backing provider's error/commit semantics. A void + * return is not an additional durable reward acknowledgement. This boundary does + * not add retries, transactions, or a second write queue. + */ +public interface SqlUserStorage { + List readRow(UserStorage storage); + + boolean contains(UserStorage storage); + + void delete(UserStorage storage); + + void write(UserStorage storage, String key, DataValue value); + + void writeValues(UserStorage storage, HashMap values); +} diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/thread/FileThread.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/thread/FileThread.java index cd99a21afe..7043b87930 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/thread/FileThread.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/thread/FileThread.java @@ -1,13 +1,6 @@ package com.bencodez.advancedcore.thread; -import java.io.File; - -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; - import com.bencodez.advancedcore.AdvancedCorePlugin; -import com.bencodez.advancedcore.api.misc.files.FilesManager; -import com.bencodez.advancedcore.api.user.UserData; /** * The Class Thread. @@ -19,100 +12,6 @@ public class FileThread { */ public class ReadThread extends java.lang.Thread { - /** - * Deletes a player file. - * - * @param uuid the player UUID - */ - @Deprecated - public void deletePlayerFile(String uuid) { - synchronized (FileThread.getInstance()) { - try { - File dFile = new File(AdvancedCorePlugin.getInstance().getDataFolder() + File.separator + "Data", - uuid + ".yml"); - if (dFile.exists()) { - dFile.delete(); - } - - } catch (Exception e) { - AdvancedCorePlugin.getInstance().debug(e); - } - } - } - - /** - * Gets data for a player. - * - * @param userData the user data - * @param uuid the player UUID - * @return the file configuration - */ - @Deprecated - public FileConfiguration getData(UserData userData, String uuid) { - synchronized (FileThread.getInstance()) { - try { - File dFile = getPlayerFile(uuid); - if (dFile != null) { - FileConfiguration data = YamlConfiguration.loadConfiguration(dFile); - return data; - } - } catch (Exception e) { - AdvancedCorePlugin.getInstance().debug(e); - } - AdvancedCorePlugin.getInstance().getLogger() - .warning("Filed to load " + uuid + ".yml, turn debug on to see full stacktraces"); - return null; - } - - } - - /** - * Gets the player file. - * - * @param uuid the player UUID - * @return the file - */ - @Deprecated - public File getPlayerFile(String uuid) { - synchronized (FileThread.getInstance()) { - try { - File dFile = new File(AdvancedCorePlugin.getInstance().getDataFolder() + File.separator + "Data", - uuid + ".yml"); - FileConfiguration data = YamlConfiguration.loadConfiguration(dFile); - if (!dFile.exists()) { - FilesManager.getInstance().editFile(dFile, data); - } - return dFile; - } catch (Exception e) { - AdvancedCorePlugin.getInstance().debug(e); - } - AdvancedCorePlugin.getInstance().getLogger() - .warning("Failed to load " + uuid + ".yml, turn debug on to see full stacktraces"); - return null; - } - } - - /** - * Checks if a player file exists. - * - * @param uuid the player UUID - * @return true if file exists - */ - @Deprecated - public boolean hasPlayerFile(String uuid) { - synchronized (FileThread.getInstance()) { - try { - File dFile = new File(AdvancedCorePlugin.getInstance().getDataFolder() + File.separator + "Data", - uuid + ".yml"); - return dFile.exists(); - - } catch (Exception e) { - AdvancedCorePlugin.getInstance().debug(e); - } - return false; - } - } - @Override public void run() { while (!isInterrupted()) { @@ -136,31 +35,6 @@ public void run(Runnable run) { } } - - /** - * Sets data for a player. - * - * @param userData the user data - * @param uuid the player UUID - * @param path the data path - * @param value the value to set - */ - @Deprecated - public void setData(UserData userData, final String uuid, final String path, final Object value) { - synchronized (FileThread.getInstance()) { - try { - File dFile = getPlayerFile(uuid); - FileConfiguration data = getData(userData, uuid); - data.set(path, value); - data.save(dFile); - } catch (Exception e) { - AdvancedCorePlugin.getInstance().getLogger().warning( - "Failed to set a value for " + uuid + ".yml, turn debug on to see full stacktraces"); - AdvancedCorePlugin.getInstance().debug(e); - } - } - - } } /** The instance. */ diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/storage/BukkitSqlUserStorageTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/storage/BukkitSqlUserStorageTest.java new file mode 100644 index 0000000000..c6c9e055d8 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/storage/BukkitSqlUserStorageTest.java @@ -0,0 +1,99 @@ +package com.bencodez.advancedcore.tests.storage; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import com.bencodez.advancedcore.AdvancedCorePlugin; +import com.bencodez.advancedcore.api.user.UserStorage; +import com.bencodez.advancedcore.api.user.userstorage.mysql.MySQL; +import com.bencodez.advancedcore.api.user.userstorage.sql.UserTable; +import com.bencodez.advancedcore.bukkit.user.storage.BukkitSqlUserStorage; +import com.bencodez.simpleapi.sql.Column; +import com.bencodez.simpleapi.sql.data.DataValue; +import com.bencodez.simpleapi.sql.data.DataValueInt; +import com.bencodez.simpleapi.sql.data.DataValueString; + +class BukkitSqlUserStorageTest { + @Test + void constructionDoesNotResolveOwnersAndReadUsesCurrentTableAndUuid() { + BukkitSqlUserStorage uninitialized = new BukkitSqlUserStorage( + () -> { throw new AssertionError("eager plugin"); }, + () -> { throw new AssertionError("eager UUID"); }); + assertNotNull(uninitialized); + AdvancedCorePlugin first = mock(AdvancedCorePlugin.class), second = mock(AdvancedCorePlugin.class); + MySQL firstTable = mock(MySQL.class), secondTable = mock(MySQL.class); + when(first.getMysql()).thenReturn(firstTable); + when(second.getMysql()).thenReturn(secondTable); + AtomicReference owner = new AtomicReference<>(first); + AtomicReference uuid = new AtomicReference<>("first-id"); + BukkitSqlUserStorage storage = new BukkitSqlUserStorage(owner::get, uuid::get); + ArrayList firstRow = new ArrayList<>(), secondRow = new ArrayList<>(); + when(firstTable.getExact("first-id")).thenReturn(firstRow); + when(secondTable.getExact("second-id")).thenReturn(secondRow); + assertSame(firstRow, storage.readRow(UserStorage.MYSQL)); + owner.set(second); + uuid.set("second-id"); + assertSame(secondRow, storage.readRow(UserStorage.MYSQL)); + storage.write(UserStorage.MYSQL, "Points", new DataValueInt(1)); + verify(secondTable).update(eq("second-id"), eq("Points"), any(DataValue.class)); + verify(first, never()).getDataFolder(); + verify(second, never()).getDataFolder(); + } + + @Test + void sqliteBulkPreservesPerEntryCallsCumulativeListAndUuidExclusion() { + AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); + UserTable table = mock(UserTable.class); + when(plugin.getSQLiteUserTable()).thenReturn(table); + BukkitSqlUserStorage storage = new BukkitSqlUserStorage(() -> plugin, () -> "target-id"); + List> writes = new ArrayList<>(); + doAnswer(call -> { + Column primary = call.getArgument(0); + assertEquals("uuid", primary.getName()); + assertEquals("target-id", primary.getValue().getString()); + ArrayList columns = call.getArgument(1); + writes.add(columns.stream().map(Column::getName).toList()); + return null; + }).when(table).update(any(Column.class), any()); + HashMap values = new LinkedHashMap<>(); + values.put("Points", new DataValueInt(1)); + values.put("uuid", new DataValueString("ignored-id")); + values.put("PlayerName", new DataValueString("Ben")); + storage.writeValues(UserStorage.SQLITE, values); + assertEquals(List.of(List.of("Points"), List.of("Points"), List.of("Points", "PlayerName")), writes); + storage.writeValues(UserStorage.SQLITE, new HashMap<>()); + assertEquals(3, writes.size()); + } + + @Test + void mysqlBulkUsesExistingNonQueuedUpdateAndMissingProviderNoOp() { + AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); + MySQL table = mock(MySQL.class); + when(plugin.getMysql()).thenReturn(table); + BukkitSqlUserStorage storage = new BukkitSqlUserStorage(() -> plugin, () -> "target-id"); + HashMap values = new LinkedHashMap<>(); + DataValue points = new DataValueInt(1); + values.put("uuid", new DataValueString("ignored-id")); + values.put("Points", points); + storage.writeValues(UserStorage.MYSQL, values); + @SuppressWarnings({"rawtypes", "unchecked"}) + ArgumentCaptor> columns = ArgumentCaptor.forClass((Class) ArrayList.class); + verify(table).update(eq("target-id"), columns.capture(), eq(false)); + assertEquals(1, columns.getValue().size()); + assertEquals("Points", columns.getValue().get(0).getName()); + assertSame(points, columns.getValue().get(0).getValue()); + when(plugin.getMysql()).thenReturn(null); + assertDoesNotThrow(() -> storage.writeValues(UserStorage.MYSQL, values)); + verifyNoMoreInteractions(table); + } +} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/storage/SqlAccessHeadlessFixture.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/storage/SqlAccessHeadlessFixture.java new file mode 100644 index 0000000000..073e231240 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/storage/SqlAccessHeadlessFixture.java @@ -0,0 +1,51 @@ +package com.bencodez.advancedcore.tests.storage; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import com.bencodez.advancedcore.api.user.UserStorage; +import com.bencodez.advancedcore.core.user.storage.SqlUserDataAccess; +import com.bencodez.advancedcore.core.user.storage.SqlUserStorage; +import com.bencodez.simpleapi.sql.Column; +import com.bencodez.simpleapi.sql.data.DataValue; + +/** Runs with only shared types; the in-memory port is not a live SQL database. */ +public final class SqlAccessHeadlessFixture { + public static void run() { + for (UserStorage backend : UserStorage.values()) { + MemoryPort port = new MemoryPort(); + SqlUserDataAccess data = new SqlUserDataAccess(port); + check(!data.hasData(backend)); + check(data.getInt(backend, "Points", -1) == -1); + check(!data.hasData(backend)); + data.setInt(backend, "Points", 7); + data.setString(backend, "PlayerName", "Ben"); + check(data.hasData(backend)); + check(data.getInt(backend, "Points", 0) == 7); + check(data.getString(backend, "PlayerName").equals("Ben")); + check(data.getKeys(backend).size() == 2); + SqlUserDataAccess second = new SqlUserDataAccess(port); + check(second.getValues(backend).size() == 2); + second.remove(backend); + check(!data.hasData(backend)); + } + } + + private static void check(boolean value) { + if (!value) throw new AssertionError("Shared SQL-access fixture failed"); + } + + private static final class MemoryPort implements SqlUserStorage { + private final HashMap values = new HashMap<>(); + @Override public List readRow(UserStorage backend) { + ArrayList row = new ArrayList<>(); + values.forEach((key, value) -> row.add(new Column(key, value))); + return row; + } + @Override public boolean contains(UserStorage backend) { return !values.isEmpty(); } + @Override public void delete(UserStorage backend) { values.clear(); } + @Override public void write(UserStorage backend, String key, DataValue value) { values.put(key, value); } + @Override public void writeValues(UserStorage backend, HashMap incoming) { values.putAll(incoming); } + } +} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/storage/SqlAccessHeadlessTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/storage/SqlAccessHeadlessTest.java new file mode 100644 index 0000000000..89354fbea4 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/storage/SqlAccessHeadlessTest.java @@ -0,0 +1,38 @@ +package com.bencodez.advancedcore.tests.storage; + +import static org.junit.jupiter.api.Assertions.*; + +import java.net.URL; +import java.net.URLClassLoader; + +import org.junit.jupiter.api.Test; + +import com.bencodez.advancedcore.core.user.storage.SqlUserDataAccess; +import com.bencodez.simpleapi.sql.Column; + +class SqlAccessHeadlessTest { + @Test + void sharedTypesAndBehaviorLoadWithBukkitAndPluginClassesDenied() throws Exception { + URL[] paths = { SqlUserDataAccess.class.getProtectionDomain().getCodeSource().getLocation(), + Column.class.getProtectionDomain().getCodeSource().getLocation(), + SqlAccessHeadlessFixture.class.getProtectionDomain().getCodeSource().getLocation() }; + try (URLClassLoader loader = new URLClassLoader(paths, ClassLoader.getPlatformClassLoader()) { + @Override protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.startsWith("org.bukkit.") || name.startsWith("io.papermc.") + || name.startsWith("net.minecraft.") || name.startsWith("net.fabricmc.") + || name.startsWith("net.minecraftforge.") || name.startsWith("net.neoforged.") + || name.startsWith("com.bencodez.advancedcore.bukkit.") + || name.equals("com.bencodez.advancedcore.AdvancedCorePlugin") + || name.equals("com.bencodez.advancedcore.api.user.AdvancedCoreUser") + || name.equals("com.bencodez.advancedcore.api.user.UserData")) { + throw new ClassNotFoundException("Platform class denied: " + name); + } + return super.loadClass(name, resolve); + } + }) { + assertThrows(ClassNotFoundException.class, () -> loader.loadClass("org.bukkit.Bukkit")); + assertThrows(ClassNotFoundException.class, () -> loader.loadClass("com.bencodez.advancedcore.AdvancedCorePlugin")); + loader.loadClass("com.bencodez.advancedcore.tests.storage.SqlAccessHeadlessFixture").getMethod("run").invoke(null); + } + } +} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/storage/SqlUserDataAccessTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/storage/SqlUserDataAccessTest.java new file mode 100644 index 0000000000..43e1671e24 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/storage/SqlUserDataAccessTest.java @@ -0,0 +1,125 @@ +package com.bencodez.advancedcore.tests.storage; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import com.bencodez.advancedcore.api.user.UserStorage; +import com.bencodez.advancedcore.core.user.storage.SqlUserDataAccess; +import com.bencodez.advancedcore.core.user.storage.SqlUserStorage; +import com.bencodez.simpleapi.sql.Column; +import com.bencodez.simpleapi.sql.data.DataValue; +import com.bencodez.simpleapi.sql.data.DataValueBoolean; +import com.bencodez.simpleapi.sql.data.DataValueInt; +import com.bencodez.simpleapi.sql.data.DataValueString; + +class SqlUserDataAccessTest { + @Test + void scalarReadsRetainTypesDefaultsCaseAndFirstMatchingStringBehavior() { + for (UserStorage backend : UserStorage.values()) { + SqlUserStorage store = mock(SqlUserStorage.class); + SqlUserDataAccess data = new SqlUserDataAccess(store); + when(store.readRow(backend)).thenReturn(List.of( + new Column("int", new DataValueInt(12)), + new Column("number", new DataValueString("34")), + new Column("invalid", new DataValueString("bad")), + new Column("invalid", new DataValueInt(99)), + new Column("null", new DataValueString("NuLl")), + new Column("empty", new DataValueString(null)), + new Column("boolean", new DataValueBoolean(true)))); + assertEquals(12, data.getInt(backend, "int", -1)); + assertEquals(34, data.getInt(backend, "number", -1)); + assertEquals(-1, data.getInt(backend, "invalid", -1)); + assertEquals(-1, data.getInt(backend, "INT", -1)); + assertEquals("", data.getString(backend, "int")); + assertEquals("34", data.getString(backend, "number")); + assertEquals("", data.getString(backend, "null")); + assertEquals("", data.getString(backend, "empty")); + assertEquals("true", data.getString(backend, "boolean")); + } + } + + @Test + void blankKeysDoNotReadAndNullRowsKeepDefaults() { + SqlUserStorage store = mock(SqlUserStorage.class); + SqlUserDataAccess data = new SqlUserDataAccess(store); + assertEquals(7, data.getInt(UserStorage.SQLITE, null, 7)); + assertEquals(7, data.getInt(UserStorage.SQLITE, "", 7)); + assertEquals("", data.getString(UserStorage.SQLITE, null)); + assertEquals("", data.getString(UserStorage.SQLITE, "")); + verifyNoInteractions(store); + when(store.readRow(UserStorage.SQLITE)).thenReturn(null); + assertEquals(7, data.getInt(UserStorage.SQLITE, "missing", 7)); + assertEquals("", data.getString(UserStorage.SQLITE, "missing")); + assertTrue(data.getKeys(UserStorage.SQLITE).isEmpty()); + assertTrue(data.getValues(UserStorage.SQLITE).isEmpty()); + } + + @Test + void rowsRemainLiveAndLegacyReaderCanOverrideTheBackingStore() { + SqlUserStorage store = mock(SqlUserStorage.class); + AtomicInteger reads = new AtomicInteger(); + AtomicReference> row = new AtomicReference<>(List.of(new Column("value", new DataValueInt(1)))); + SqlUserDataAccess data = new SqlUserDataAccess(store, backend -> { + reads.incrementAndGet(); + return row.get(); + }); + assertEquals(0, reads.get()); + assertEquals(1, data.getInt(UserStorage.MYSQL, "value", 0)); + row.set(List.of(new Column("value", new DataValueInt(2)))); + assertEquals(2, data.getInt(UserStorage.MYSQL, "value", 0)); + assertEquals(2, reads.get()); + verifyNoInteractions(store); + } + + @Test + void mapAndKeyExportsPreserveValueIdentityDuplicatesAndMutability() { + SqlUserStorage store = mock(SqlUserStorage.class); + SqlUserDataAccess data = new SqlUserDataAccess(store); + DataValue first = new DataValueInt(1), last = new DataValueInt(2); + when(store.readRow(UserStorage.SQLITE)).thenReturn(List.of(new Column("value", first), new Column("value", last))); + ArrayList keys = data.getKeys(UserStorage.SQLITE); + assertEquals(List.of("value", "value"), keys); + keys.add("local"); + HashMap values = data.getValues(UserStorage.SQLITE); + assertSame(last, values.get("value")); + values.put("local", first); + assertEquals(1, data.getValues(UserStorage.SQLITE).size()); + assertTrue(SqlUserDataAccess.convert(null).isEmpty()); + } + + @Test + void writesExistenceDeletionAndExceptionsUseTheSuppliedStore() { + SqlUserStorage store = mock(SqlUserStorage.class); + SqlUserDataAccess data = new SqlUserDataAccess(store); + data.setInt(UserStorage.MYSQL, "Points", 42); + data.setString(UserStorage.SQLITE, "PlayerName", null); + ArgumentCaptor value = ArgumentCaptor.forClass(DataValue.class); + verify(store).write(eq(UserStorage.MYSQL), eq("Points"), value.capture()); + assertTrue(value.getValue().isInt()); + assertEquals(42, value.getValue().getInt()); + verify(store).write(eq(UserStorage.SQLITE), eq("PlayerName"), value.capture()); + assertTrue(value.getValue().isString()); + assertEquals("", value.getValue().getString()); + HashMap values = new HashMap<>(); + data.setValues(UserStorage.SQLITE, values); + verify(store).writeValues(UserStorage.SQLITE, values); + when(store.contains(UserStorage.MYSQL)).thenReturn(true); + assertTrue(data.hasData(UserStorage.MYSQL)); + data.remove(UserStorage.MYSQL); + verify(store).delete(UserStorage.MYSQL); + IllegalStateException failure = new IllegalStateException("database unavailable"); + when(store.readRow(UserStorage.MYSQL)).thenThrow(failure); + assertSame(failure, assertThrows(IllegalStateException.class, () -> data.getInt(UserStorage.MYSQL, "x", 0))); + doThrow(failure).when(store).delete(UserStorage.SQLITE); + assertSame(failure, assertThrows(IllegalStateException.class, () -> data.remove(UserStorage.SQLITE))); + } +} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/storage/SqlUserDataFacadeTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/storage/SqlUserDataFacadeTest.java new file mode 100644 index 0000000000..d6cda4f2c0 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/storage/SqlUserDataFacadeTest.java @@ -0,0 +1,132 @@ +package com.bencodez.advancedcore.tests.storage; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledExecutorService; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import com.bencodez.advancedcore.AdvancedCorePlugin; +import com.bencodez.advancedcore.api.user.AdvancedCoreUser; +import com.bencodez.advancedcore.api.user.UserData; +import com.bencodez.advancedcore.api.user.UserDataFetchMode; +import com.bencodez.advancedcore.api.user.UserManager; +import com.bencodez.advancedcore.api.user.UserStorage; +import com.bencodez.advancedcore.api.user.usercache.UserDataCache; +import com.bencodez.advancedcore.api.user.usercache.change.UserDataChangeInt; +import com.bencodez.advancedcore.api.user.userstorage.mysql.MySQL; +import com.bencodez.simpleapi.sql.Column; +import com.bencodez.simpleapi.sql.data.DataValue; +import com.bencodez.simpleapi.sql.data.DataValueInt; + +class SqlUserDataFacadeTest { + @Test + void legacyRowAndConvertOverridesRemainVirtualWithoutPluginAccess() { + AdvancedCoreUser user = mock(AdvancedCoreUser.class); + ArrayList row = new ArrayList<>(List.of(new Column("Points", new DataValueInt(17)))); + HashMap converted = new HashMap<>(); + UserData data = new UserData(user) { + @Override public List getMySqlRow() { return row; } + @Override public List getSQLiteRow() { return row; } + @Override public HashMap convert(List columns) { + assertSame(row, columns); + return converted; + } + }; + for (UserStorage storage : UserStorage.values()) { + assertEquals(17, data.getInt(storage, "Points", 0, UserDataFetchMode.NO_CACHE)); + assertEquals(List.of("Points"), data.getKeys(storage)); + assertSame(converted, data.getValues(storage)); + } + verifyNoInteractions(user); + } + + @Test + void everyFetchModeRetainsTempUserCacheAndStoragePrecedence() { + for (UserDataFetchMode mode : UserDataFetchMode.values()) { + Fixture f = new Fixture(); + UserDataCache cache = mock(UserDataCache.class); + when(f.user.getCache()).thenReturn(cache); + when(cache.isCached("Points")).thenReturn(true); + HashMap userValues = new HashMap<>(); + userValues.put("Points", new DataValueInt(2)); + when(cache.getCache()).thenReturn(userValues); + HashMap temp = new HashMap<>(); + temp.put("Points", new DataValueInt(3)); + f.data.setTempCache(temp); + assertEquals(mode.allowTempCache() ? 3 : 2, f.data.getInt("Points", -1, mode)); + verifyNoInteractions(f.table); + temp.clear(); + when(cache.isCached("Points")).thenReturn(false); + when(f.table.getExact("initial-id")).thenReturn(new ArrayList<>(List.of(new Column("Points", new DataValueInt(4))))); + assertEquals(mode.allowStorageLookup() ? 4 : -1, f.data.getInt("Points", -1, mode)); + verify(f.table, times(mode.allowStorageLookup() ? 1 : 0)).getExact("initial-id"); + } + } + + @Test + void queuedCachedWriteDoesNotReachSqlOrCreateAnotherQueue() { + Fixture f = new Fixture(); + UserDataCache cache = mock(UserDataCache.class); + when(f.user.isCached()).thenReturn(true); + when(f.user.getCache()).thenReturn(cache); + f.data.setInt("Points", 19, true, true); + var order = inOrder(cache, f.manager); + order.verify(cache).addChange(any(UserDataChangeInt.class), eq(true)); + order.verify(f.manager).onChange(f.user, "Points"); + verifyNoInteractions(f.table, f.timer); + } + + @Test + void asyncWriteResolvesUuidAtExecutionAndNotifiesOnlyAfterSql() { + Fixture f = new Fixture(); + f.data.setInt("Points", 19, false, true); + ArgumentCaptor task = ArgumentCaptor.forClass(Runnable.class); + verify(f.timer).execute(task.capture()); + verifyNoInteractions(f.table, f.manager); + when(f.user.getUUID()).thenReturn("current-id"); + task.getValue().run(); + var order = inOrder(f.table, f.manager); + order.verify(f.table).update(eq("current-id"), eq("Points"), any(DataValue.class)); + order.verify(f.manager).onChange(f.user, "Points"); + } + + @Test + void storageFailureDoesNotClearCacheAndSchedulerRejectionDoesNotWrite() { + Fixture f = new Fixture(); + IllegalStateException failure = new IllegalStateException("delete failed"); + doThrow(failure).when(f.table).deletePlayer("initial-id"); + assertSame(failure, assertThrows(IllegalStateException.class, f.data::remove)); + verify(f.user, never()).clearCache(); + clearInvocations(f.table); + RejectedExecutionException rejected = new RejectedExecutionException("stopped"); + doThrow(rejected).when(f.timer).execute(any(Runnable.class)); + assertSame(rejected, assertThrows(RejectedExecutionException.class, () -> f.data.setInt("Points", 1, false, true))); + verifyNoInteractions(f.table, f.manager); + } + + private static final class Fixture { + final AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); + final AdvancedCoreUser user = mock(AdvancedCoreUser.class); + final MySQL table = mock(MySQL.class); + final UserManager manager = mock(UserManager.class); + final ScheduledExecutorService timer = mock(ScheduledExecutorService.class); + final UserData data = new UserData(user); + Fixture() { + when(user.getPlugin()).thenReturn(plugin); + when(user.getUUID()).thenReturn("initial-id"); + when(user.getPlayerName()).thenReturn("Ben"); + when(plugin.getStorageType()).thenReturn(UserStorage.MYSQL); + when(plugin.getMysql()).thenReturn(table); + when(plugin.getUserManager()).thenReturn(manager); + when(plugin.getTimer()).thenReturn(timer); + } + } +} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/user/SqlOnlyUserDataTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/user/SqlOnlyUserDataTest.java new file mode 100644 index 0000000000..9603099187 --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/user/SqlOnlyUserDataTest.java @@ -0,0 +1,115 @@ +package com.bencodez.advancedcore.tests.user; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import com.bencodez.advancedcore.AdvancedCorePlugin; +import com.bencodez.advancedcore.api.user.AdvancedCoreUser; +import com.bencodez.advancedcore.api.user.UserData; +import com.bencodez.advancedcore.api.user.UserDataFetchMode; +import com.bencodez.advancedcore.api.user.UserManager; +import com.bencodez.advancedcore.api.user.UserStorage; +import com.bencodez.advancedcore.api.user.userstorage.mysql.MySQL; +import com.bencodez.advancedcore.api.user.userstorage.sql.UserTable; +import com.bencodez.simpleapi.sql.Column; +import com.bencodez.simpleapi.sql.data.DataValue; +import com.bencodez.simpleapi.sql.data.DataValueInt; +import com.bencodez.simpleapi.sql.data.DataValueString; + +class SqlOnlyUserDataTest { + @Test + void bothSqlBackendsRetainReadAndExistenceBehavior() { + for (UserStorage storage : UserStorage.values()) { + Fixture fixture = new Fixture(storage); + ArrayList row = new ArrayList<>(List.of(new Column("Points", new DataValueInt(12)), + new Column("PlayerName", new DataValueString("Ben")))); + when(fixture.mysql.getExact(fixture.id)).thenReturn(row); + when(fixture.sqlite.getExact(any(Column.class))).thenReturn(row); + when(fixture.mysql.containsKey(fixture.id)).thenReturn(true); + when(fixture.sqlite.containsKey(fixture.id)).thenReturn(true); + assertEquals(12, fixture.data.getInt("Points", 0, UserDataFetchMode.NO_CACHE)); + assertEquals("Ben", fixture.data.getString("PlayerName", UserDataFetchMode.NO_CACHE)); + assertEquals(List.of("Points", "PlayerName"), fixture.data.getKeys()); + assertEquals(2, fixture.data.getValues().size()); + assertTrue(fixture.data.hasData()); + verify(fixture.plugin, never()).getDataFolder(); + } + } + + @Test + void scalarWritesKeepUuidAndSqlValueTypes() { + Fixture sqlite = new Fixture(UserStorage.SQLITE); + sqlite.data.setInt("Points", 24, false); + sqlite.data.setString("PlayerName", "Ben", false); + ArgumentCaptor key = ArgumentCaptor.forClass(Column.class); + @SuppressWarnings({ "rawtypes", "unchecked" }) + ArgumentCaptor> rows = ArgumentCaptor.forClass((Class) ArrayList.class); + verify(sqlite.sqlite, times(2)).update(key.capture(), rows.capture()); + assertEquals(sqlite.id, key.getValue().getValue().getString()); + assertEquals("uuid", rows.getAllValues().get(0).get(0).getName()); + assertEquals(24, rows.getAllValues().get(0).get(1).getValue().getInt()); + assertEquals("Ben", rows.getAllValues().get(1).get(1).getValue().getString()); + verify(sqlite.manager).onChange(sqlite.user, "Points"); + verify(sqlite.manager).onChange(sqlite.user, "PlayerName"); + + Fixture mysql = new Fixture(UserStorage.MYSQL); + mysql.data.setInt("Points", 24, false); + mysql.data.setString("PlayerName", "Ben", false); + ArgumentCaptor values = ArgumentCaptor.forClass(DataValue.class); + verify(mysql.mysql).update(eq(mysql.id), eq("Points"), values.capture()); + assertEquals(24, values.getValue().getInt()); + verify(mysql.mysql).update(eq(mysql.id), eq("PlayerName"), values.capture()); + assertEquals("Ben", values.getValue().getString()); + verify(mysql.plugin, never()).getDataFolder(); + verify(sqlite.plugin, never()).getDataFolder(); + } + + @Test + void removalUsesSelectedSqlTableThenClearsExistingUserCache() { + for (UserStorage storage : UserStorage.values()) { + Fixture fixture = new Fixture(storage); + fixture.data.remove(); + if (storage == UserStorage.MYSQL) { + var order = inOrder(fixture.mysql, fixture.user); + order.verify(fixture.mysql).deletePlayer(fixture.id); + order.verify(fixture.user).clearCache(); + } else { + ArgumentCaptor key = ArgumentCaptor.forClass(Column.class); + var order = inOrder(fixture.sqlite, fixture.user); + order.verify(fixture.sqlite).delete(key.capture()); + order.verify(fixture.user).clearCache(); + assertEquals("uuid", key.getValue().getName()); + assertEquals(fixture.id, key.getValue().getValue().getString()); + } + verify(fixture.plugin, never()).getDataFolder(); + } + } + + private static final class Fixture { + final String id = UUID.randomUUID().toString(); + final AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class); + final AdvancedCoreUser user = mock(AdvancedCoreUser.class); + final MySQL mysql = mock(MySQL.class); + final UserTable sqlite = mock(UserTable.class); + final UserManager manager = mock(UserManager.class); + final UserData data = new UserData(user); + + Fixture(UserStorage storage) { + when(user.getPlugin()).thenReturn(plugin); + when(user.getUUID()).thenReturn(id); + when(user.getPlayerName()).thenReturn("Ben"); + when(plugin.getStorageType()).thenReturn(storage); + when(plugin.getMysql()).thenReturn(mysql); + when(plugin.getSQLiteUserTable()).thenReturn(sqlite); + when(plugin.getUserManager()).thenReturn(manager); + } + } +} diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/user/UserStorageTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/user/UserStorageTest.java index 79017c79d1..76e730bd28 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/user/UserStorageTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/tests/user/UserStorageTest.java @@ -15,7 +15,6 @@ public class UserStorageTest { public void testValue_caseInsensitiveMatches() { assertEquals(UserStorage.MYSQL, UserStorage.value("mysql")); assertEquals(UserStorage.SQLITE, UserStorage.value("SQLITE")); - assertEquals(UserStorage.FLAT, UserStorage.value("flat")); } @Test @@ -24,4 +23,18 @@ public void testValue_unknownReturnsNull() { assertNull(UserStorage.value("")); assertNull(UserStorage.value(" ")); } + + @Test + public void flatIsNotAnAvailableStorageType() { + assertArrayEquals(new UserStorage[] { UserStorage.MYSQL, UserStorage.SQLITE }, UserStorage.values()); + assertThrows(IllegalArgumentException.class, () -> UserStorage.valueOf("FLAT")); + } + + @Test + public void retiredFlatConfigurationFailsRatherThanSelectingAnEmptySqlDatabase() { + for (String value : new String[] { "FLAT", "flat", "Flat", " FLAT " }) { + IllegalArgumentException failure = assertThrows(IllegalArgumentException.class, () -> UserStorage.value(value)); + assertTrue(failure.getMessage().contains("previous version")); + } + } } diff --git a/docs/shared-sql-user-access.md b/docs/shared-sql-user-access.md new file mode 100644 index 0000000000..fe37dba9b9 --- /dev/null +++ b/docs/shared-sql-user-access.md @@ -0,0 +1,46 @@ +# Shared SQL user access + +This follows removal of FLAT user storage. `core.user.storage.SqlUserStorage` +is a game-independent, per-user SQL access contract, and `SqlUserDataAccess` +implements row interpretation and typed reads/writes against it. A native host +can supply a storage implementation without an `AdvancedCorePlugin` instance. + +`BukkitSqlUserStorage` reuses the existing MySQL and SQLite user-table providers. +It resolves the plugin, provider, and UUID lazily at operation time. It neither +constructs connections nor owns a cache, executor, identity resolver, schema, or +shutdown lifecycle. Shared access performs no game-thread dispatch. SQL calls +may block and must remain on the caller's appropriate storage execution context. + +The existing `UserData` facade delegates its SQL operations to this access layer. +Its temporary-cache field and accessors, six fetch modes, user-cache precedence, +cache updates, notifications, list encoding, and synchronous/asynchronous write +scheduling stay in place. Row reads still dispatch through the virtual +`getMySqlRow()` / `getSQLiteRow()` methods, and `getValues` still calls the virtual +`convert` method. Reload/replacement does not leave the adapter holding old tables +or an eagerly captured UUID. No new cache or queue is introduced. + +The port deliberately preserves existing provider behavior, including the +MySQL missing-provider bulk-write no-op and SQLite cumulative per-entry bulk +updates. It does not claim that void-returning provider methods establish durable +completion, atomic batches, or successful disk commits. Async reward completion +and checkpoint work in PR #317 remain separate; do not substitute this interface +for that work's completion contract. + +## Scope and validation + +This is the SQL-access boundary, not the complete native user runtime. Existing +Bukkit-facing provider construction, SQLite connection/table bootstrap, schema +registration, `UserManager` enumeration, and user-cache lifecycle still need +native wiring. The current full AdvancedCore JAR is not a Fabric/Forge/NeoForge +artifact. No alternate database implementation or duplicate JDBC stack is added. + +Tests cover typed/default/case behavior, live rows, subclass hooks, map identity, +backend delegation, bulk call ordering, lazy ownership, all existing fetch modes, +queued writes, asynchronous identity resolution, deletion/rejection failures, and +isolated shared-class loading. The headless fixture uses an in-memory port; it is +not a database integration test. Preserve existing SQL tests as well. + +One Maven project, existing package layout, SimpleAPI `1.0.2-SNAPSHOT`, no new +workflow or dependency pin. Merge the FLAT-removal prerequisite first, reconcile +#317's overlapping user/cache work, and validate the exact candidate through +AdvancedCore and VotingPlugin builds plus packaged database/server tests. diff --git a/docs/sql-only-user-storage.md b/docs/sql-only-user-storage.md new file mode 100644 index 0000000000..c730fad214 --- /dev/null +++ b/docs/sql-only-user-storage.md @@ -0,0 +1,24 @@ +# SQL-only user storage + +User storage now supports `SQLITE` and `MYSQL` only. `FLAT` is removed from the +enum, normal reads/writes, enumeration, column operations, startup, and deprecated +per-player file helpers. Existing `Data/*.yml` files are neither deleted nor +converted by this change. Ordinary YAML configuration and generic file utilities +remain available. + +An explicit `DataStorage: FLAT` setting fails with an explanatory error instead +of silently starting with an empty SQL database. Complete conversion with the +existing conversion commands in the previous version before upgrading, then +select the populated `SQLITE` or `MYSQL` backend. No new migration, automatic +storage switch, schema change, or rewrite of the existing SQL converter is added. +SQL-to-SQL conversion remains available through `convertDataStorage`. + +This deliberately removes the deprecated `UserStorage.FLAT` enum constant, +`UserData.getData(String)`, and the per-player file CRUD methods on +`FileThread.ReadThread`. Integrations still calling those FLAT-only APIs must +migrate before upgrading. SQL-facing public APIs, fetch modes, cached changes, +write scheduling, serialized values, and reward queues are unchanged. + +The follow-up SQL abstraction builds on this removal. Coordinate both branches +with changes to user data/cache code in PR #317; this removal does not merge or +modify that PR. SimpleAPI remains `1.0.2-SNAPSHOT` in the single Maven project.