From 6df8e895165d770a9160daeba0c2993b8a66d0a8 Mon Sep 17 00:00:00 2001 From: Minecraft0122 <168195378+Minecraft0122@users.noreply.github.com> Date: Sun, 9 Aug 2026 17:10:23 +0800 Subject: [PATCH] Cache unchanged extension metadata --- .../gathering/DataValueGatherer.java | 49 ++--- .../gathering/ExtensionMetadataStorage.java | 201 ++++++++++++++++++ .../database/transactions/Transaction.java | 8 +- .../ExtensionMetadataStorageTest.java | 183 ++++++++++++++++ .../TransactionExecutionStateTest.java | 86 ++++++++ 5 files changed, 493 insertions(+), 34 deletions(-) create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/extension/implementation/providers/gathering/ExtensionMetadataStorage.java create mode 100644 Plan/common/src/test/java/com/djrapitops/plan/extension/implementation/providers/gathering/ExtensionMetadataStorageTest.java create mode 100644 Plan/common/src/test/java/com/djrapitops/plan/storage/database/transactions/TransactionExecutionStateTest.java diff --git a/Plan/common/src/main/java/com/djrapitops/plan/extension/implementation/providers/gathering/DataValueGatherer.java b/Plan/common/src/main/java/com/djrapitops/plan/extension/implementation/providers/gathering/DataValueGatherer.java index c21d45ef18..687b4b48d7 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/extension/implementation/providers/gathering/DataValueGatherer.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/extension/implementation/providers/gathering/DataValueGatherer.java @@ -37,8 +37,6 @@ import com.djrapitops.plan.extension.implementation.storage.transactions.StoreIconTransaction; import com.djrapitops.plan.extension.implementation.storage.transactions.StorePluginTransaction; import com.djrapitops.plan.extension.implementation.storage.transactions.StoreTabInformationTransaction; -import com.djrapitops.plan.extension.implementation.storage.transactions.providers.StoreProviderTransaction; -import com.djrapitops.plan.extension.implementation.storage.transactions.providers.StoreTableProviderTransaction; import com.djrapitops.plan.extension.implementation.storage.transactions.results.*; import com.djrapitops.plan.extension.table.Table; import com.djrapitops.plan.identification.ServerInfo; @@ -70,6 +68,7 @@ public class DataValueGatherer { private final ErrorLogger errorLogger; private final Set brokenMethods; + private final ExtensionMetadataStorage metadataStorage; public DataValueGatherer( ExtensionWrapper extension, @@ -86,6 +85,7 @@ public DataValueGatherer( this.errorLogger = errorLogger; this.brokenMethods = new HashSet<>(); + this.metadataStorage = new ExtensionMetadataStorage(); } public boolean shouldSkipEvent(CallEvents event) { @@ -477,8 +477,7 @@ private void storeBoolean(Parameters parameters, Conditions conditions, BooleanD } Database db = dbSystem.getDatabase(); - db.executeTransaction(new StoreIconTransaction(information.getIcon())); - db.executeTransaction(new StoreProviderTransaction(information, parameters)); + metadataStorage.storeProvider(db, information, parameters); db.executeTransaction(new StoreServerBooleanResultTransaction(information, parameters, value)); } @@ -488,8 +487,7 @@ private void storeNumber(Parameters parameters, Conditions conditions, NumberDat if (value == null) return; Database db = dbSystem.getDatabase(); - db.executeTransaction(new StoreIconTransaction(information.getIcon())); - db.executeTransaction(new StoreProviderTransaction(information, parameters)); + metadataStorage.storeProvider(db, information, parameters); db.executeTransaction(new StoreServerNumberResultTransaction(information, parameters, value)); } @@ -500,8 +498,7 @@ private void storeDouble(Parameters parameters, Conditions conditions, DoubleDat if (value == null) return; Database db = dbSystem.getDatabase(); - db.executeTransaction(new StoreIconTransaction(information.getIcon())); - db.executeTransaction(new StoreProviderTransaction(information, parameters)); + metadataStorage.storeProvider(db, information, parameters); db.executeTransaction(new StoreServerDoubleResultTransaction(information, parameters, value)); } @@ -511,8 +508,7 @@ private void storeString(Parameters parameters, Conditions conditions, StringDat if (value == null) return; Database db = dbSystem.getDatabase(); - db.executeTransaction(new StoreIconTransaction(information.getIcon())); - db.executeTransaction(new StoreProviderTransaction(information, parameters)); + metadataStorage.storeProvider(db, information, parameters); db.executeTransaction(new StoreServerStringResultTransaction(information, parameters, value)); } @@ -522,8 +518,7 @@ private void storeComponent(Parameters parameters, Conditions conditions, Compon if (value == null) return; Database db = dbSystem.getDatabase(); - db.executeTransaction(new StoreIconTransaction(information.getIcon())); - db.executeTransaction(new StoreProviderTransaction(information, parameters)); + metadataStorage.storeProvider(db, information, parameters); db.executeTransaction(new StoreServerStringResultTransaction(information, parameters, value)); } @@ -533,10 +528,7 @@ private void storeTable(Parameters parameters, Conditions conditions, TableDataV if (value == null) return; Database db = dbSystem.getDatabase(); - for (Icon icon : value.getIcons()) { - if (icon != null) db.executeTransaction(new StoreIconTransaction(icon)); - } - db.executeTransaction(new StoreTableProviderTransaction(information, parameters, value)); + metadataStorage.storeTableProvider(db, information, parameters, value); db.executeTransaction(new StoreServerTableResultTransaction(information, parameters, value)); } @@ -551,8 +543,7 @@ private void storePlayerBoolean(Parameters parameters, Conditions conditions, Bo } Database db = dbSystem.getDatabase(); - db.executeTransaction(new StoreIconTransaction(information.getIcon())); - db.executeTransaction(new StoreProviderTransaction(information, parameters)); + metadataStorage.storeProvider(db, information, parameters); db.executeTransaction(new StorePlayerBooleanResultTransaction(information, parameters, value)); } @@ -562,8 +553,7 @@ private void storePlayerNumber(Parameters parameters, Conditions conditions, Num if (value == null) return; Database db = dbSystem.getDatabase(); - db.executeTransaction(new StoreIconTransaction(information.getIcon())); - db.executeTransaction(new StoreProviderTransaction(information, parameters)); + metadataStorage.storeProvider(db, information, parameters); db.executeTransaction(new StorePlayerNumberResultTransaction(information, parameters, value)); } @@ -573,8 +563,7 @@ private void storePlayerDouble(Parameters parameters, Conditions conditions, Dou if (value == null) return; Database db = dbSystem.getDatabase(); - db.executeTransaction(new StoreIconTransaction(information.getIcon())); - db.executeTransaction(new StoreProviderTransaction(information, parameters)); + metadataStorage.storeProvider(db, information, parameters); db.executeTransaction(new StorePlayerDoubleResultTransaction(information, parameters, value)); } @@ -584,8 +573,7 @@ private void storePlayerString(Parameters parameters, Conditions conditions, Str if (value == null) return; Database db = dbSystem.getDatabase(); - db.executeTransaction(new StoreIconTransaction(information.getIcon())); - db.executeTransaction(new StoreProviderTransaction(information, parameters)); + metadataStorage.storeProvider(db, information, parameters); db.executeTransaction(new StorePlayerStringResultTransaction(information, parameters, value)); } @@ -595,8 +583,7 @@ private void storePlayerComponent(Parameters parameters, Conditions conditions, if (value == null) return; Database db = dbSystem.getDatabase(); - db.executeTransaction(new StoreIconTransaction(information.getIcon())); - db.executeTransaction(new StoreProviderTransaction(information, parameters)); + metadataStorage.storeProvider(db, information, parameters); db.executeTransaction(new StorePlayerStringResultTransaction(information, parameters, value)); } @@ -606,8 +593,7 @@ private void storePlayerGroups(Parameters parameters, Conditions conditions, Gro if (value == null) return; Database db = dbSystem.getDatabase(); - db.executeTransaction(new StoreIconTransaction(information.getIcon())); - db.executeTransaction(new StoreProviderTransaction(information, parameters)); + metadataStorage.storeProvider(db, information, parameters); db.executeTransaction(new StorePlayerGroupsResultTransaction(information, parameters, value)); } @@ -617,10 +603,7 @@ private void storePlayerTable(Parameters parameters, Conditions conditions, Tabl if (value == null) return; Database db = dbSystem.getDatabase(); - for (Icon icon : value.getIcons()) { - if (icon != null) db.executeTransaction(new StoreIconTransaction(icon)); - } - db.executeTransaction(new StoreTableProviderTransaction(information, parameters, value)); + metadataStorage.storeTableProvider(db, information, parameters, value); db.executeTransaction(new StorePlayerTableResultTransaction(information, parameters, value)); } -} \ No newline at end of file +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/extension/implementation/providers/gathering/ExtensionMetadataStorage.java b/Plan/common/src/main/java/com/djrapitops/plan/extension/implementation/providers/gathering/ExtensionMetadataStorage.java new file mode 100644 index 0000000000..8a24030de8 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/extension/implementation/providers/gathering/ExtensionMetadataStorage.java @@ -0,0 +1,201 @@ +/* + * This file is part of Player Analytics (Plan). + * + * Plan is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License v3 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Plan is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Plan. If not, see . + */ +package com.djrapitops.plan.extension.implementation.providers.gathering; + +import com.djrapitops.plan.extension.icon.Icon; +import com.djrapitops.plan.extension.implementation.ProviderInformation; +import com.djrapitops.plan.extension.implementation.providers.Parameters; +import com.djrapitops.plan.extension.implementation.storage.transactions.StoreIconTransaction; +import com.djrapitops.plan.extension.implementation.storage.transactions.providers.StoreProviderTransaction; +import com.djrapitops.plan.extension.implementation.storage.transactions.providers.StoreTableProviderTransaction; +import com.djrapitops.plan.extension.table.Table; +import com.djrapitops.plan.identification.ServerUUID; +import com.djrapitops.plan.storage.database.Database; +import com.djrapitops.plan.storage.database.transactions.Transaction; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +/** + * Avoids writing unchanged extension metadata for every gathered value. + */ +class ExtensionMetadataStorage { + + private final Map storedMetadata = new HashMap<>(); + + public synchronized void storeProvider( + Database database, + ProviderInformation information, + Parameters parameters + ) { + MetadataKey key = new MetadataKey(false, parameters.getServerUUID(), information.getPluginName(), information.getName()); + MetadataFingerprint fingerprint = MetadataFingerprint.forProvider(information); + if (fingerprint.equals(storedMetadata.get(key))) return; + + storedMetadata.put(key, fingerprint); + StoreProviderTransaction providerTransaction = new StoreProviderTransaction(information, parameters); + try { + database.executeTransaction(new StoreIconTransaction(information.getIcon())); + invalidateIfNotStored(database.executeTransaction(providerTransaction), providerTransaction, key, fingerprint); + } catch (RuntimeException executionFailure) { + storedMetadata.remove(key, fingerprint); + throw executionFailure; + } + } + + public synchronized void storeTableProvider( + Database database, + ProviderInformation information, + Parameters parameters, + Table table + ) { + MetadataKey key = new MetadataKey(true, parameters.getServerUUID(), information.getPluginName(), information.getName()); + MetadataFingerprint fingerprint = MetadataFingerprint.forTableProvider(information, parameters, table); + if (fingerprint.equals(storedMetadata.get(key))) return; + + storedMetadata.put(key, fingerprint); + StoreTableProviderTransaction providerTransaction = new StoreTableProviderTransaction(information, parameters, table); + try { + Icon[] icons = table.getIcons(); + for (int i = 0; i < table.getMaxColumnSize(); i++) { + database.executeTransaction(new StoreIconTransaction(icons[i])); + } + invalidateIfNotStored(database.executeTransaction(providerTransaction), providerTransaction, key, fingerprint); + } catch (RuntimeException executionFailure) { + storedMetadata.remove(key, fingerprint); + throw executionFailure; + } + } + + private void invalidateIfNotStored( + CompletableFuture completion, + Transaction transaction, + MetadataKey key, + MetadataFingerprint fingerprint + ) { + completion.whenComplete((result, failure) -> { + if (failure != null || !transaction.wasExecuted()) { + synchronized (ExtensionMetadataStorage.this) { + storedMetadata.remove(key, fingerprint); + } + } + }); + } + + private static final class MetadataKey { + private final boolean tableProvider; + private final ServerUUID serverUUID; + private final String pluginName; + private final String providerName; + + private MetadataKey(boolean tableProvider, ServerUUID serverUUID, String pluginName, String providerName) { + this.tableProvider = tableProvider; + this.serverUUID = serverUUID; + this.pluginName = pluginName; + this.providerName = providerName; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (!(other instanceof MetadataKey)) return false; + MetadataKey that = (MetadataKey) other; + return tableProvider == that.tableProvider && + Objects.equals(serverUUID, that.serverUUID) && + Objects.equals(pluginName, that.pluginName) && + Objects.equals(providerName, that.providerName); + } + + @Override + public int hashCode() { + return Objects.hash(tableProvider, serverUUID, pluginName, providerName); + } + } + + private static final class MetadataFingerprint { + private final List values; + + private MetadataFingerprint(List values) { + this.values = values; + } + + private static MetadataFingerprint forProvider(ProviderInformation information) { + return new MetadataFingerprint(providerInformation(information)); + } + + private static MetadataFingerprint forTableProvider( + ProviderInformation information, + Parameters parameters, + Table table + ) { + List values = providerInformation(information); + values.add(parameters.getMethodType()); + values.addAll(Arrays.asList(table.getColumns().clone())); + Icon[] icons = table.getIcons(); + for (int i = 0; i < table.getMaxColumnSize(); i++) { + addIcon(values, icons[i]); + } + values.addAll(Arrays.asList(table.getTableColumnFormats().clone())); + return new MetadataFingerprint(values); + } + + private static List providerInformation(ProviderInformation information) { + List values = new ArrayList<>(); + values.add(information.getPluginName()); + values.add(information.getName()); + values.add(information.getText()); + values.add(information.getDescription().orElse(null)); + values.add(information.getPriority()); + addIcon(values, information.getIcon()); + values.add(information.isShownInPlayersTable()); + values.add(information.getTab().orElse(null)); + values.add(information.getCondition().orElse(null)); + values.add(information.isHidden()); + values.add(information.getProvidedCondition()); + values.add(information.getFormatType().orElse(null)); + values.add(information.isPlayerName()); + values.add(information.getTableColor()); + values.add(information.isPercentage()); + values.add(information.isComponent()); + return values; + } + + private static void addIcon(List values, Icon icon) { + values.add(icon.getFamily()); + values.add(icon.getName()); + values.add(icon.getColor()); + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (!(other instanceof MetadataFingerprint)) return false; + MetadataFingerprint that = (MetadataFingerprint) other; + return values.equals(that.values); + } + + @Override + public int hashCode() { + return values.hashCode(); + } + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/Transaction.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/Transaction.java index 2295a35a9b..aeeec6b63f 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/Transaction.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/Transaction.java @@ -49,6 +49,7 @@ public abstract class Transaction { protected DBType dbType; protected boolean success; protected int attempts; + private boolean executed; private SQLDB db; private Connection connection; private Savepoint savepoint; @@ -85,6 +86,7 @@ public void executeTransaction(SQLDB db) { } performOperations(); if (connection != null) connection.commit(); + executed = true; } success = true; } catch (SQLException statementFail) { @@ -287,6 +289,10 @@ public boolean wasSuccessful() { return success; } + public boolean wasExecuted() { + return executed; + } + public boolean dbIsNotUnderHeavyLoad() { return !db.isUnderHeavyLoad() && !db.shouldDropUnimportantTransactions(); } @@ -317,4 +323,4 @@ public enum IsolationLevel { UNCHANGED, READ_COMMITTED } -} \ No newline at end of file +} diff --git a/Plan/common/src/test/java/com/djrapitops/plan/extension/implementation/providers/gathering/ExtensionMetadataStorageTest.java b/Plan/common/src/test/java/com/djrapitops/plan/extension/implementation/providers/gathering/ExtensionMetadataStorageTest.java new file mode 100644 index 0000000000..a6624a300f --- /dev/null +++ b/Plan/common/src/test/java/com/djrapitops/plan/extension/implementation/providers/gathering/ExtensionMetadataStorageTest.java @@ -0,0 +1,183 @@ +/* + * This file is part of Player Analytics (Plan). + * + * Plan is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License v3 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Plan is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Plan. If not, see . + */ +package com.djrapitops.plan.extension.implementation.providers.gathering; + +import com.djrapitops.plan.extension.icon.Icon; +import com.djrapitops.plan.extension.implementation.ProviderInformation; +import com.djrapitops.plan.extension.implementation.providers.Parameters; +import com.djrapitops.plan.extension.implementation.storage.transactions.StoreIconTransaction; +import com.djrapitops.plan.extension.implementation.storage.transactions.providers.StoreProviderTransaction; +import com.djrapitops.plan.extension.implementation.storage.transactions.providers.StoreTableProviderTransaction; +import com.djrapitops.plan.extension.table.Table; +import com.djrapitops.plan.storage.database.Database; +import com.djrapitops.plan.storage.database.transactions.Transaction; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import utilities.TestConstants; + +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ExtensionMetadataStorageTest { + + @Mock + Database database; + + private ExtensionMetadataStorage underTest; + private Parameters parameters; + private List submitted; + private Map> completions; + + @BeforeEach + void setUp() { + underTest = new ExtensionMetadataStorage(); + parameters = Parameters.server(TestConstants.SERVER_UUID); + submitted = new ArrayList<>(); + completions = new IdentityHashMap<>(); + when(database.executeTransaction(any())).thenAnswer(invocation -> { + Transaction transaction = invocation.getArgument(0); + CompletableFuture completion = new CompletableFuture<>(); + submitted.add(transaction); + completions.put(transaction, completion); + return completion; + }); + } + + @Test + void unchangedProviderMetadataIsOnlySubmittedOnce() { + ProviderInformation information = provider("Online", "Online players"); + + underTest.storeProvider(database, information, parameters); + underTest.storeProvider(database, information, parameters); + + assertEquals(1, count(StoreIconTransaction.class)); + assertEquals(1, count(StoreProviderTransaction.class)); + } + + @Test + void changedProviderMetadataIsSubmittedAgain() { + underTest.storeProvider(database, provider("Online", "Online players"), parameters); + underTest.storeProvider(database, provider("Online", "Players online now"), parameters); + + assertEquals(2, count(StoreIconTransaction.class)); + assertEquals(2, count(StoreProviderTransaction.class)); + } + + @Test + void providerMetadataIsStoredForEachServer() { + ProviderInformation information = provider("Online", "Online players"); + + underTest.storeProvider(database, information, parameters); + underTest.storeProvider(database, information, Parameters.server(TestConstants.SERVER_TWO_UUID)); + + assertEquals(2, count(StoreIconTransaction.class)); + assertEquals(2, count(StoreProviderTransaction.class)); + } + + @Test + void changingOnlyTableRowsDoesNotRewriteTableMetadata() { + ProviderInformation information = provider("Top players", "Top players"); + Table first = Table.builder() + .columnOne("Player", Icon.called("user").build()) + .addRow("Alice") + .build(); + Table second = Table.builder() + .columnOne("Player", Icon.called("user").build()) + .addRow("Bob") + .build(); + + underTest.storeTableProvider(database, information, parameters, first); + underTest.storeTableProvider(database, information, parameters, second); + + assertEquals(1, count(StoreIconTransaction.class)); + assertEquals(1, count(StoreTableProviderTransaction.class)); + } + + @Test + void changedTableColumnsRewriteTableMetadata() { + ProviderInformation information = provider("Top players", "Top players"); + Table first = Table.builder() + .columnOne("Player", Icon.called("user").build()) + .addRow("Alice") + .build(); + Table second = Table.builder() + .columnOne("Player name", Icon.called("user").build()) + .addRow("Alice") + .build(); + + underTest.storeTableProvider(database, information, parameters, first); + underTest.storeTableProvider(database, information, parameters, second); + + assertEquals(2, count(StoreIconTransaction.class)); + assertEquals(2, count(StoreTableProviderTransaction.class)); + } + + @Test + void skippedMetadataTransactionIsNotCached() { + ProviderInformation information = provider("Online", "Online players"); + underTest.storeProvider(database, information, parameters); + Transaction providerTransaction = submitted.stream() + .filter(StoreProviderTransaction.class::isInstance) + .findFirst() + .orElseThrow(); + + completions.get(providerTransaction).complete(null); + underTest.storeProvider(database, information, parameters); + + assertEquals(2, count(StoreIconTransaction.class)); + assertEquals(2, count(StoreProviderTransaction.class)); + } + + @Test + void failedMetadataTransactionIsNotCached() { + ProviderInformation information = provider("Online", "Online players"); + underTest.storeProvider(database, information, parameters); + Transaction providerTransaction = submitted.stream() + .filter(StoreProviderTransaction.class::isInstance) + .findFirst() + .orElseThrow(); + + completions.get(providerTransaction).completeExceptionally(new IllegalStateException("Database unavailable")); + underTest.storeProvider(database, information, parameters); + + assertEquals(2, count(StoreIconTransaction.class)); + assertEquals(2, count(StoreProviderTransaction.class)); + } + + private ProviderInformation provider(String name, String text) { + return ProviderInformation.builder("TestExtension") + .setName(name) + .setText(text) + .setIcon(Icon.called("cube").build()) + .build(); + } + + private long count(Class type) { + return submitted.stream().filter(type::isInstance).count(); + } +} diff --git a/Plan/common/src/test/java/com/djrapitops/plan/storage/database/transactions/TransactionExecutionStateTest.java b/Plan/common/src/test/java/com/djrapitops/plan/storage/database/transactions/TransactionExecutionStateTest.java new file mode 100644 index 0000000000..3793d3e27a --- /dev/null +++ b/Plan/common/src/test/java/com/djrapitops/plan/storage/database/transactions/TransactionExecutionStateTest.java @@ -0,0 +1,86 @@ +/* + * This file is part of Player Analytics (Plan). + * + * Plan is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License v3 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Plan is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Plan. If not, see . + */ +package com.djrapitops.plan.storage.database.transactions; + +import com.djrapitops.plan.storage.database.DBType; +import com.djrapitops.plan.storage.database.SQLDB; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Savepoint; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class TransactionExecutionStateTest { + + @Mock + SQLDB database; + @Mock + Connection connection; + @Mock + Savepoint savepoint; + + @BeforeEach + void setUp() throws SQLException { + when(database.getType()).thenReturn(DBType.SQLITE); + when(database.getConnection()).thenReturn(connection); + } + + @Test + void executedTransactionReportsExecution() throws SQLException { + when(connection.setSavepoint()).thenReturn(savepoint); + Transaction transaction = new Transaction() { + @Override + protected void performOperations() { + // No database operations are needed for execution-state tracking. + } + }; + + transaction.executeTransaction(database); + + assertTrue(transaction.wasSuccessful()); + assertTrue(transaction.wasExecuted()); + } + + @Test + void skippedTransactionDoesNotReportExecution() { + Transaction transaction = new Transaction() { + @Override + protected boolean shouldBeExecuted() { + return false; + } + + @Override + protected void performOperations() { + throw new AssertionError("Skipped transaction was executed"); + } + }; + + transaction.executeTransaction(database); + + assertTrue(transaction.wasSuccessful()); + assertFalse(transaction.wasExecuted()); + } +}