diff --git a/Mage.Client/src/main/java/mage/client/MageFrame.java b/Mage.Client/src/main/java/mage/client/MageFrame.java index 6fa8d09649f..69c452ded05 100644 --- a/Mage.Client/src/main/java/mage/client/MageFrame.java +++ b/Mage.Client/src/main/java/mage/client/MageFrame.java @@ -1774,7 +1774,7 @@ public class MageFrame extends javax.swing.JFrame implements MageClient { private void doClientShutdownAndExit() { tablesPane.cleanUp(); - CardRepository.instance.closeDB(); + CardRepository.instance.closeDB(true); Plugins.instance.shutdown(); dispose(); System.exit(0); diff --git a/Mage.Common/src/main/java/mage/db/EntityManager.java b/Mage.Common/src/main/java/mage/db/EntityManager.java index 3938b9ab1ce..81eb25c83a0 100644 --- a/Mage.Common/src/main/java/mage/db/EntityManager.java +++ b/Mage.Common/src/main/java/mage/db/EntityManager.java @@ -5,9 +5,8 @@ import com.j256.ormlite.dao.DaoManager; import com.j256.ormlite.jdbc.JdbcConnectionSource; import com.j256.ormlite.support.ConnectionSource; import com.j256.ormlite.table.TableUtils; +import mage.cards.repository.DatabaseUtils; import mage.db.model.Feedback; -import mage.db.model.Log; -import mage.utils.properties.PropertiesUtil; import java.io.File; import java.sql.SQLException; @@ -21,20 +20,15 @@ public enum EntityManager { instance; - private Dao logDao; private Dao feedbackDao; - private EntityManager() { + EntityManager() { File file = new File("db"); if (!file.exists()) { file.mkdirs(); } try { - ConnectionSource logConnectionSource = new JdbcConnectionSource(PropertiesUtil.getDBLogUrl()); - TableUtils.createTableIfNotExists(logConnectionSource, Log.class); - logDao = DaoManager.createDao(logConnectionSource, Log.class); - - ConnectionSource feedbackConnectionSource = new JdbcConnectionSource(PropertiesUtil.getDBFeedbackUrl()); + ConnectionSource feedbackConnectionSource = new JdbcConnectionSource(DatabaseUtils.prepareH2Connection(DatabaseUtils.DB_NAME_FEEDBACK, false)); TableUtils.createTableIfNotExists(feedbackConnectionSource, Feedback.class); feedbackDao = DaoManager.createDao(feedbackConnectionSource, Feedback.class); } catch (SQLException ex) { @@ -42,22 +36,6 @@ public enum EntityManager { } } - public void insertLog(String key, java.util.Date date, String... args) throws SQLException { - Log logEntity = new Log(key, date); - logEntity.setArguments(args); - logDao.create(logEntity); - } - - public List getAllLogs() { - List logs = new ArrayList<>(); - try { - logs = logDao.queryForAll(); - } catch (SQLException ex) { - } - - return logs; - } - public void insertFeedback(String username, String title, String type, String message, String email, String host, java.util.Date created) throws SQLException { Feedback feedback = new Feedback(username, title, type, message, email, host, created, "new"); feedbackDao.create(feedback); diff --git a/Mage.Common/src/main/java/mage/db/EntityManagerTest.java b/Mage.Common/src/main/java/mage/db/EntityManagerTest.java index 38bba68a29a..18c9fb2b0a7 100644 --- a/Mage.Common/src/main/java/mage/db/EntityManagerTest.java +++ b/Mage.Common/src/main/java/mage/db/EntityManagerTest.java @@ -12,26 +12,7 @@ import java.util.List; */ public final class EntityManagerTest { - private static DateFormat timeFormatter = SimpleDateFormat.getTimeInstance(SimpleDateFormat.FULL); - public static void main(String[] args) throws Exception { - List logs = EntityManager.instance.getAllLogs(); - System.out.println("logs found: " + logs.size()); - for (Log log : logs) { - System.out.println(" key=" + log.getKey()); - System.out.println(" date=" + timeFormatter.format(log.getCreatedDate())); - System.out.print(" arguments=[ "); - if (log.getArguments() != null) { - for (String argument : log.getArguments()) { - System.out.print("arg=" + argument + ' '); - } - } - System.out.println("]"); - System.out.println(" --------------"); - } - - System.out.println("********************************"); - List feedbackList = EntityManager.instance.getAllFeedbacks(); System.out.println("feedbacks found: " + feedbackList.size()); int count = 1; diff --git a/Mage.Common/src/main/java/mage/db/Statistics.java b/Mage.Common/src/main/java/mage/db/Statistics.java deleted file mode 100644 index cdfe7d2d7b1..00000000000 --- a/Mage.Common/src/main/java/mage/db/Statistics.java +++ /dev/null @@ -1,113 +0,0 @@ -package mage.db; - -import mage.db.model.Log; - -import java.util.*; - -/** - * @author noxx - */ -public final class Statistics { - - public static void main(String[] args) throws Exception { - List logs = EntityManager.instance.getAllLogs(); - System.out.println("logs found: " + logs.size()); - - Map nicknames = displayCommonNumbers(logs); - List games = displayTop3(nicknames); - displayPlayedOnlyOnce(games); - - System.out.println("Done"); - } - - private static void displayPlayedOnlyOnce(List games) { - Integer oneGame = 0; - for (Integer numberOfGames : games) { - if (numberOfGames == 1) { - oneGame++; - } - } - - System.out.println("Number of players played only one game: " + oneGame); - } - - private static List displayTop3(Map nicknames) { - Collection values = nicknames.values(); - List games = new ArrayList<>(); - games.addAll(values); - Collections.sort(games, new Comparator() { - @Override - public int compare(Integer i1, Integer i2) { - return i2.compareTo(i1); - } - }); - - // Top-3 - List numbersToFind = new ArrayList<>(); - for (Integer numberOfGames : games) { - numbersToFind.add(numberOfGames); - if (numbersToFind.size() == 3) { - break; - } - } - - Map players = new LinkedHashMap<>(); - for (Map.Entry entry : nicknames.entrySet()) { - if (check(numbersToFind, entry.getValue())) { - players.put(entry.getValue(), entry.getKey()); - } - if (players.size() == 3) { - break; - } - } - - System.out.println("Top-3"); - for (Map.Entry entry : players.entrySet()) { - System.out.println(" " + entry.getValue() + ": " + entry.getKey()); - } - return games; - } - - private static Map displayCommonNumbers(List logs) { - int count = 0; - Map nicknames = new HashMap<>(); - for (Log log : logs) { - if (log.getKey().equals("gameStarted")) { - if (log.getArguments() != null) { - int index = 0; - for (String argument : log.getArguments()) { - if (index > 0) { - inc(nicknames, argument); - } - index++; - } - } - count++; - } - } - - System.out.println("********************************"); - System.out.println("Games played: " + count); - System.out.println("Number of players: " + nicknames.size()); - return nicknames; - } - - public static void inc(Map map, String player) { - if (map.containsKey(player)) { - Integer count = map.get(player); - count++; - map.put(player, count); - } else { - map.put(player, 1); - } - } - - public static boolean check(List numbers, Integer value) { - for (Integer number : numbers) { - if (number.equals(value)) { - return true; - } - } - return false; - } -} diff --git a/Mage.Common/src/main/java/mage/utils/properties/PropertiesUtil.java b/Mage.Common/src/main/java/mage/utils/properties/PropertiesUtil.java deleted file mode 100644 index 17fe10e05b1..00000000000 --- a/Mage.Common/src/main/java/mage/utils/properties/PropertiesUtil.java +++ /dev/null @@ -1,59 +0,0 @@ -package mage.utils.properties; - -import org.apache.log4j.Logger; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; - -/** - * @author noxx - */ -public final class PropertiesUtil { - - private static final Logger logger = Logger.getLogger(PropertiesUtil.class); - - private static final String LOG_JDBC_URL = "jdbc:h2:file:./db/mage.h2;AUTO_SERVER=TRUE"; - private static final String FEEDBACK_JDBC_URL = "jdbc:h2:file:./db/feedback.h2;AUTO_SERVER=TRUE"; - - private static Properties properties = new Properties(); - - static { - try (InputStream in = PropertiesUtil.class.getResourceAsStream("/xmage.properties")) { - if(in != null) { - properties.load(in); - } else { - logger.warn("No xmage.properties were found"); - } - } catch (FileNotFoundException fnfe) { - logger.warn("No xmage.properties were found on classpath"); - } catch (IOException e) { - logger.error("Couldn't load properties"); - e.printStackTrace(); - } - } - - /** - * Hide constructor - */ - private PropertiesUtil() { - - } - - public static String getDBLogUrl () { - String url = properties.getProperty(PropertyKeys.KEY_DB_LOG_URL, LOG_JDBC_URL); - if (url != null) { - return url.trim(); - } - return null; - } - - public static String getDBFeedbackUrl () { - String url = properties.getProperty(PropertyKeys.KEY_DB_FEEDBACK_URL, FEEDBACK_JDBC_URL); - if (url != null) { - return url.trim(); - } - return null; - } - } diff --git a/Mage.Common/src/main/java/mage/utils/properties/PropertyKeys.java b/Mage.Common/src/main/java/mage/utils/properties/PropertyKeys.java deleted file mode 100644 index 15b3f72dc9b..00000000000 --- a/Mage.Common/src/main/java/mage/utils/properties/PropertyKeys.java +++ /dev/null @@ -1,10 +0,0 @@ -package mage.utils.properties; - -/** - * @author noxx - */ -public final class PropertyKeys { - - public static final String KEY_DB_LOG_URL = "db.log.url"; - public static final String KEY_DB_FEEDBACK_URL = "db.feedback.url"; -} diff --git a/Mage.Server/src/main/java/mage/server/AuthorizedUserRepository.java b/Mage.Server/src/main/java/mage/server/AuthorizedUserRepository.java index 2f817d7db03..9526cc52c99 100644 --- a/Mage.Server/src/main/java/mage/server/AuthorizedUserRepository.java +++ b/Mage.Server/src/main/java/mage/server/AuthorizedUserRepository.java @@ -10,6 +10,7 @@ import com.j256.ormlite.support.ConnectionSource; import com.j256.ormlite.support.DatabaseConnection; import com.j256.ormlite.table.TableUtils; import mage.cards.repository.CardRepository; +import mage.cards.repository.DatabaseUtils; import mage.cards.repository.RepositoryUtil; import org.apache.log4j.Logger; import org.apache.shiro.crypto.RandomNumberGenerator; @@ -24,7 +25,6 @@ import java.util.List; public class AuthorizedUserRepository { - private static final String JDBC_URL = "jdbc:h2:file:./db/authorized_user.h2;AUTO_SERVER=TRUE"; private static final String VERSION_ENTITY_NAME = "authorized_user"; // raise this if db structure was changed private static final long DB_VERSION = 2; @@ -32,10 +32,10 @@ public class AuthorizedUserRepository { private static final AuthorizedUserRepository instance; static { - instance = new AuthorizedUserRepository(JDBC_URL); + instance = new AuthorizedUserRepository(DatabaseUtils.prepareH2Connection(DatabaseUtils.DB_NAME_USERS, false)); } - private Dao dao; + private Dao usersDao; public AuthorizedUserRepository(String connectionString) { File file = new File("db"); @@ -45,7 +45,7 @@ public class AuthorizedUserRepository { try { ConnectionSource connectionSource = new JdbcConnectionSource(connectionString); TableUtils.createTableIfNotExists(connectionSource, AuthorizedUser.class); - dao = DaoManager.createDao(connectionSource, AuthorizedUser.class); + usersDao = DaoManager.createDao(connectionSource, AuthorizedUser.class); } catch (SQLException ex) { Logger.getLogger(AuthorizedUserRepository.class).error("Error creating / assigning authorized_user repository - ", ex); } @@ -59,7 +59,7 @@ public class AuthorizedUserRepository { try { Hash hash = new SimpleHash(Sha256Hash.ALGORITHM_NAME, password, rng.nextBytes(), 1024); AuthorizedUser user = new AuthorizedUser(userName, hash, email); - dao.create(user); + usersDao.create(user); } catch (SQLException ex) { Logger.getLogger(AuthorizedUserRepository.class).error("Error adding a user to DB - ", ex); } @@ -67,7 +67,7 @@ public class AuthorizedUserRepository { public void remove(final String userName) { try { - DeleteBuilder db = dao.deleteBuilder(); + DeleteBuilder db = usersDao.deleteBuilder(); db.where().eq("name", new SelectArg(userName)); db.delete(); } catch (SQLException ex) { @@ -77,9 +77,9 @@ public class AuthorizedUserRepository { public AuthorizedUser getByName(String userName) { try { - QueryBuilder qb = dao.queryBuilder(); + QueryBuilder qb = usersDao.queryBuilder(); qb.where().eq("name", new SelectArg(userName)); - List results = dao.query(qb.prepare()); + List results = usersDao.query(qb.prepare()); if (results.size() == 1) { return results.get(0); } @@ -92,7 +92,7 @@ public class AuthorizedUserRepository { public void update(AuthorizedUser authorizedUser) { try { - dao.update(authorizedUser); + usersDao.update(authorizedUser); } catch (SQLException ex) { Logger.getLogger(AuthorizedUserRepository.class).error("Error updating authorized_user", ex); } @@ -100,9 +100,9 @@ public class AuthorizedUserRepository { public AuthorizedUser getByEmail(String userName) { try { - QueryBuilder qb = dao.queryBuilder(); + QueryBuilder qb = usersDao.queryBuilder(); qb.where().eq("email", new SelectArg(userName)); - List results = dao.query(qb.prepare()); + List results = usersDao.query(qb.prepare()); if (results.size() == 1) { return results.get(0); } @@ -115,9 +115,10 @@ public class AuthorizedUserRepository { public void closeDB() { try { - if (dao != null && dao.getConnectionSource() != null) { - DatabaseConnection conn = dao.getConnectionSource().getReadWriteConnection(dao.getTableName()); - conn.executeStatement("shutdown compact", 0); + if (usersDao != null && usersDao.getConnectionSource() != null) { + DatabaseConnection conn = usersDao.getConnectionSource().getReadWriteConnection(usersDao.getTableName()); + conn.executeStatement("SHUTDOWN IMMEDIATELY", 0); + usersDao.getConnectionSource().releaseConnection(conn); } } catch (SQLException ex) { Logger.getLogger(AuthorizedUserRepository.class).error("Error closing authorized_user repository - ", ex); @@ -126,7 +127,7 @@ public class AuthorizedUserRepository { public long getDBVersionFromDB() { try { - ConnectionSource connectionSource = new JdbcConnectionSource(JDBC_URL); + ConnectionSource connectionSource = new JdbcConnectionSource(DatabaseUtils.prepareH2Connection(DatabaseUtils.DB_NAME_USERS, false)); return RepositoryUtil.getDatabaseVersion(connectionSource, VERSION_ENTITY_NAME); } catch (SQLException ex) { Logger.getLogger(CardRepository.class).error("Error getting DB version from DB - ", ex); @@ -145,11 +146,11 @@ public class AuthorizedUserRepository { private boolean migrateFrom1To2() { try { Logger.getLogger(AuthorizedUserRepository.class).info("Starting " + VERSION_ENTITY_NAME + " DB migration from version 1 to version 2"); - dao.executeRaw("ALTER TABLE authorized_user ADD COLUMN active BOOLEAN DEFAULT true;"); - dao.executeRaw("ALTER TABLE authorized_user ADD COLUMN lockedUntil DATETIME;"); - dao.executeRaw("ALTER TABLE authorized_user ADD COLUMN chatLockedUntil DATETIME;"); - dao.executeRaw("ALTER TABLE authorized_user ADD COLUMN lastConnection DATETIME;"); - RepositoryUtil.updateVersion(dao.getConnectionSource(), VERSION_ENTITY_NAME, DB_VERSION); + usersDao.executeRaw("ALTER TABLE authorized_user ADD COLUMN active BOOLEAN DEFAULT true;"); + usersDao.executeRaw("ALTER TABLE authorized_user ADD COLUMN lockedUntil DATETIME;"); + usersDao.executeRaw("ALTER TABLE authorized_user ADD COLUMN chatLockedUntil DATETIME;"); + usersDao.executeRaw("ALTER TABLE authorized_user ADD COLUMN lastConnection DATETIME;"); + RepositoryUtil.updateVersion(usersDao.getConnectionSource(), VERSION_ENTITY_NAME, DB_VERSION); Logger.getLogger(AuthorizedUserRepository.class).info("Migration finished."); return true; } catch (SQLException ex) { diff --git a/Mage.Server/src/main/java/mage/server/record/TableRecordRepository.java b/Mage.Server/src/main/java/mage/server/record/TableRecordRepository.java index d6d7997c41f..13acb082c1d 100644 --- a/Mage.Server/src/main/java/mage/server/record/TableRecordRepository.java +++ b/Mage.Server/src/main/java/mage/server/record/TableRecordRepository.java @@ -8,6 +8,7 @@ import com.j256.ormlite.stmt.SelectArg; import com.j256.ormlite.support.ConnectionSource; import com.j256.ormlite.support.DatabaseConnection; import com.j256.ormlite.table.TableUtils; +import mage.cards.repository.DatabaseUtils; import mage.cards.repository.RepositoryUtil; import org.apache.log4j.Logger; @@ -20,12 +21,11 @@ public enum TableRecordRepository { instance; - private static final String JDBC_URL = "jdbc:sqlite:./db/table_record.db"; private static final String VERSION_ENTITY_NAME = "table_record"; // raise this if db structure was changed private static final long DB_VERSION = 0; - private Dao dao; + private Dao recordsDao; TableRecordRepository() { File file = new File("db"); @@ -33,7 +33,7 @@ public enum TableRecordRepository { file.mkdirs(); } try { - ConnectionSource connectionSource = new JdbcConnectionSource(JDBC_URL); + ConnectionSource connectionSource = new JdbcConnectionSource(DatabaseUtils.prepareSqliteConnection(DatabaseUtils.DB_NAME_RECORDS)); boolean obsolete = RepositoryUtil.isDatabaseObsolete(connectionSource, VERSION_ENTITY_NAME, DB_VERSION); if (obsolete) { @@ -41,7 +41,7 @@ public enum TableRecordRepository { } TableUtils.createTableIfNotExists(connectionSource, TableRecord.class); - dao = DaoManager.createDao(connectionSource, TableRecord.class); + recordsDao = DaoManager.createDao(connectionSource, TableRecord.class); } catch (SQLException ex) { Logger.getLogger(TableRecordRepository.class).error("Error creating table_record repository - ", ex); } @@ -49,7 +49,7 @@ public enum TableRecordRepository { public void add(TableRecord tableHistory) { try { - dao.create(tableHistory); + recordsDao.create(tableHistory); } catch (SQLException ex) { Logger.getLogger(TableRecordRepository.class).error("Error adding a table_record to DB - ", ex); } @@ -57,10 +57,10 @@ public enum TableRecordRepository { public List getAfter(long endTimeMs) { try { - QueryBuilder qb = dao.queryBuilder(); + QueryBuilder qb = recordsDao.queryBuilder(); qb.where().gt("endTimeMs", new SelectArg(endTimeMs)); qb.orderBy("endTimeMs", true); - return dao.query(qb.prepare()); + return recordsDao.query(qb.prepare()); } catch (SQLException ex) { Logger.getLogger(TableRecordRepository.class).error("Error getting table_records from DB - ", ex); } @@ -69,9 +69,10 @@ public enum TableRecordRepository { public void closeDB() { try { - if (dao != null && dao.getConnectionSource() != null) { - DatabaseConnection conn = dao.getConnectionSource().getReadWriteConnection(dao.getTableName()); - conn.executeStatement("shutdown compact", 0); + if (recordsDao != null && recordsDao.getConnectionSource() != null) { + DatabaseConnection conn = recordsDao.getConnectionSource().getReadWriteConnection(recordsDao.getTableName()); + conn.executeStatement("SHUTDOWN IMMEDIATELY", 0); + recordsDao.getConnectionSource().releaseConnection(conn); } } catch (SQLException ex) { Logger.getLogger(TableRecordRepository.class).error("Error closing table_record repository - ", ex); diff --git a/Mage.Server/src/main/java/mage/server/record/UserStatsRepository.java b/Mage.Server/src/main/java/mage/server/record/UserStatsRepository.java index 0bbe5cab296..5af86d7f035 100644 --- a/Mage.Server/src/main/java/mage/server/record/UserStatsRepository.java +++ b/Mage.Server/src/main/java/mage/server/record/UserStatsRepository.java @@ -8,6 +8,7 @@ import com.j256.ormlite.stmt.SelectArg; import com.j256.ormlite.support.ConnectionSource; import com.j256.ormlite.support.DatabaseConnection; import com.j256.ormlite.table.TableUtils; +import mage.cards.repository.DatabaseUtils; import mage.cards.repository.RepositoryUtil; import mage.game.result.ResultProtos; import mage.server.rating.GlickoRating; @@ -22,12 +23,11 @@ public enum UserStatsRepository { instance; - private static final String JDBC_URL = "jdbc:sqlite:./db/user_stats.db"; private static final String VERSION_ENTITY_NAME = "user_stats"; // raise this if db structure was changed private static final long DB_VERSION = 0; - private Dao dao; + private Dao statsDao; UserStatsRepository() { File file = new File("db"); @@ -35,7 +35,7 @@ public enum UserStatsRepository { file.mkdirs(); } try { - ConnectionSource connectionSource = new JdbcConnectionSource(JDBC_URL); + ConnectionSource connectionSource = new JdbcConnectionSource(DatabaseUtils.prepareSqliteConnection(DatabaseUtils.DB_NAME_STATS)); boolean obsolete = RepositoryUtil.isDatabaseObsolete(connectionSource, VERSION_ENTITY_NAME, DB_VERSION); if (obsolete) { @@ -43,7 +43,7 @@ public enum UserStatsRepository { } TableUtils.createTableIfNotExists(connectionSource, UserStats.class); - dao = DaoManager.createDao(connectionSource, UserStats.class); + statsDao = DaoManager.createDao(connectionSource, UserStats.class); } catch (SQLException ex) { Logger.getLogger(UserStatsRepository.class).error("Error creating user_stats repository - ", ex); } @@ -51,7 +51,7 @@ public enum UserStatsRepository { public void add(UserStats userStats) { try { - dao.create(userStats); + statsDao.create(userStats); } catch (SQLException ex) { Logger.getLogger(UserStatsRepository.class).error("Error adding a user_stats to DB - ", ex); } @@ -59,7 +59,7 @@ public enum UserStatsRepository { public void update(UserStats userStats) { try { - dao.update(userStats); + statsDao.update(userStats); } catch (SQLException ex) { Logger.getLogger(UserStatsRepository.class).error("Error updating a user_stats in DB - ", ex); } @@ -67,9 +67,9 @@ public enum UserStatsRepository { public UserStats getUser(String userName) { try { - QueryBuilder qb = dao.queryBuilder(); + QueryBuilder qb = statsDao.queryBuilder(); qb.limit(1L).where().eq("userName", new SelectArg(userName)); - List users = dao.query(qb.prepare()); + List users = statsDao.query(qb.prepare()); if (!users.isEmpty()) { return users.get(0); } @@ -81,8 +81,8 @@ public enum UserStatsRepository { public List getAllUsers() { try { - QueryBuilder qb = dao.queryBuilder(); - return dao.query(qb.prepare()); + QueryBuilder qb = statsDao.queryBuilder(); + return statsDao.query(qb.prepare()); } catch (SQLException ex) { Logger.getLogger(UserStatsRepository.class).error("Error getting all users from DB - ", ex); } @@ -91,9 +91,9 @@ public enum UserStatsRepository { public long getLatestEndTimeMs() { try { - QueryBuilder qb = dao.queryBuilder(); + QueryBuilder qb = statsDao.queryBuilder(); qb.orderBy("endTimeMs", false).limit(1L); - List users = dao.query(qb.prepare()); + List users = statsDao.query(qb.prepare()); if (!users.isEmpty()) { return users.get(0).getEndTimeMs(); } @@ -366,9 +366,10 @@ public enum UserStatsRepository { public void closeDB() { try { - if (dao != null && dao.getConnectionSource() != null) { - DatabaseConnection conn = dao.getConnectionSource().getReadWriteConnection(dao.getTableName()); - conn.executeStatement("shutdown compact", 0); + if (statsDao != null && statsDao.getConnectionSource() != null) { + DatabaseConnection conn = statsDao.getConnectionSource().getReadWriteConnection(statsDao.getTableName()); + conn.executeStatement("SHUTDOWN IMMEDIATELY", 0); + statsDao.getConnectionSource().releaseConnection(conn); } } catch (SQLException ex) { Logger.getLogger(UserStatsRepository.class).error("Error closing user_stats repository - ", ex); diff --git a/Mage.Sets/src/mage/cards/b/BrainPry.java b/Mage.Sets/src/mage/cards/b/BrainPry.java index c4eed9a37ca..430185d877a 100644 --- a/Mage.Sets/src/mage/cards/b/BrainPry.java +++ b/Mage.Sets/src/mage/cards/b/BrainPry.java @@ -24,7 +24,7 @@ public final class BrainPry extends CardImpl { public BrainPry(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{B}"); - //Name a nonland card. Target player reveals their hand. That player discards a card with that name. If they can't, you draw a card. + // Name a nonland card. Target player reveals their hand. That player discards a card with that name. If they can't, you draw a card. this.getSpellAbility().addEffect((new ChooseACardNameEffect(ChooseACardNameEffect.TypeOfName.NON_LAND_NAME))); this.getSpellAbility().addTarget(new TargetPlayer()); this.getSpellAbility().addEffect(new BrainPryEffect()); diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/DatabaseBigQueryPerformanceTest.java b/Mage.Tests/src/test/java/org/mage/test/serverside/DatabaseBigQueryPerformanceTest.java new file mode 100644 index 00000000000..75ba541bab2 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/DatabaseBigQueryPerformanceTest.java @@ -0,0 +1,62 @@ +package org.mage.test.serverside; + +import mage.cards.repository.CardRepository; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; + +/** + * @author JayDi85 + */ +public class DatabaseBigQueryPerformanceTest extends CardTestPlayerBaseWithAIHelps { + + @Test + public void test_GetLands_SQL() { + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + Assert.assertTrue("must load getNames", CardRepository.instance.getNames().size() > 1000); + Assert.assertTrue("must load getNonLandNames", CardRepository.instance.getNonLandNames().size() > 1000); + Assert.assertTrue("must load getArtifactNames", CardRepository.instance.getArtifactNames().size() > 1000); + Assert.assertTrue("must load getCreatureNames", CardRepository.instance.getCreatureNames().size() > 1000); + Assert.assertTrue("must load getNonArtifactAndNonLandNames", CardRepository.instance.getNonArtifactAndNonLandNames().size() > 1000); + Assert.assertTrue("must load getNonLandAndNonCreatureNames", CardRepository.instance.getNonLandAndNonCreatureNames().size() > 1000); + } + + @Test + public void test_GetLands_RealGame_Manual() { + // Name a nonland card. Target player reveals their hand. That player discards a card with that name. + // If they can't, you draw a card. + addCard(Zone.HAND, playerA, "Brain Pry", 1); // {1}{B} + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Brain Pry", playerA); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + setChoice(playerA, "Balduvian Bears"); // name to choose + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + } + + @Test + public void test_GetLands_RealGame_AI() { + // possible bug: game simulations can call big queries multiple times and overflow database cache to crash it + // how-to fix: increase CACHE_SIZE in DatabaseUtils (require 150 000 kb on 2024) + int cardsAmount = 5; + + // Name a nonland card. Target player reveals their hand. That player discards a card with that name. + // If they can't, you draw a card. + addCard(Zone.HAND, playerA, "Brain Pry", cardsAmount); // {1}{B} + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2 * cardsAmount); + + aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/DatabaseCompatibleTest.java b/Mage.Tests/src/test/java/org/mage/test/serverside/DatabaseCompatibleTest.java index 4066f45ac77..ef5adf69d7b 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/DatabaseCompatibleTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/DatabaseCompatibleTest.java @@ -19,8 +19,6 @@ import java.nio.file.Paths; */ public class DatabaseCompatibleTest { - private final String JDBC_URL = "jdbc:h2:file:%s;AUTO_SERVER=TRUE"; - @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); @@ -38,9 +36,8 @@ public class DatabaseCompatibleTest { ); Assert.assertTrue(Files.exists(Paths.get(dbFullFileName))); - AuthorizedUserRepository dbUsers = new AuthorizedUserRepository( - String.format(JDBC_URL, dbFullName) - ); + String connectionString = String.format("jdbc:h2:file:%s;AUTO_SERVER=TRUE", dbFullName); + AuthorizedUserRepository dbUsers = new AuthorizedUserRepository(connectionString); // search Assert.assertNotNull(dbUsers.getByName("user1")); diff --git a/Mage/src/main/java/mage/cards/repository/CardRepository.java b/Mage/src/main/java/mage/cards/repository/CardRepository.java index 9bef7dccd62..523f97f8ebf 100644 --- a/Mage/src/main/java/mage/cards/repository/CardRepository.java +++ b/Mage/src/main/java/mage/cards/repository/CardRepository.java @@ -2,6 +2,7 @@ package mage.cards.repository; import com.j256.ormlite.dao.Dao; import com.j256.ormlite.dao.DaoManager; +import com.j256.ormlite.dao.GenericRawResults; import com.j256.ormlite.jdbc.JdbcConnectionSource; import com.j256.ormlite.stmt.QueryBuilder; import com.j256.ormlite.stmt.SelectArg; @@ -19,6 +20,7 @@ import java.io.File; import java.sql.SQLException; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; /** * @author North, JayDi85 @@ -31,16 +33,15 @@ public enum CardRepository { // fixes limit for out of memory problems private static final AtomicInteger databaseFixes = new AtomicInteger(); - private static final int MAX_DATABASE_FIXES = 3; - private static final String JDBC_URL = "jdbc:h2:file:./db/cards.h2;AUTO_SERVER=TRUE;IGNORECASE=TRUE"; + private static final int MAX_DATABASE_FIXES = 10; + + // TODO: delete db version from cards and expansions due un-used (cause dbs re-created on each update now) private static final String VERSION_ENTITY_NAME = "card"; - // raise this if db structure was changed - private static final long CARD_DB_VERSION = 54; - // raise this if new cards were added to the server - private static final long CARD_CONTENT_VERSION = 241; - private Dao cardDao; - private Set classNames; + private static final long CARD_DB_VERSION = 54; // raise this if db structure was changed + private static final long CARD_CONTENT_VERSION = 241; // raise this if new cards were added to the server + + private Dao cardsDao; // sets with exclusively snow basics public static final Set snowLandSetCodes = new HashSet<>(Arrays.asList( @@ -55,7 +56,7 @@ public enum CardRepository { file.mkdirs(); } try { - ConnectionSource connectionSource = new JdbcConnectionSource(JDBC_URL); + ConnectionSource connectionSource = new JdbcConnectionSource(DatabaseUtils.prepareH2Connection(DatabaseUtils.DB_NAME_CARDS, true)); boolean isObsolete = RepositoryUtil.isDatabaseObsolete(connectionSource, VERSION_ENTITY_NAME, CARD_DB_VERSION); boolean isNewBuild = RepositoryUtil.isNewBuildRun(connectionSource, VERSION_ENTITY_NAME, CardRepository.class); // recreate db on new build @@ -65,7 +66,7 @@ public enum CardRepository { } TableUtils.createTableIfNotExists(connectionSource, CardInfo.class); - cardDao = DaoManager.createDao(connectionSource, CardInfo.class); + cardsDao = DaoManager.createDao(connectionSource, CardInfo.class); } catch (SQLException e) { Logger.getLogger(CardRepository.class).error("Error creating card repository - " + e, e); processMemoryErrors(e); @@ -95,26 +96,22 @@ public enum CardRepository { } public void saveCards(final List newCards, long newContentVersion) { + if (newCards == null || newCards.isEmpty()) { + return; + } + try { - cardDao.callBatchTasks(() -> { - // add - if (newCards != null && !newCards.isEmpty()) { - logger.info("DB: need to add " + newCards.size() + " new cards"); - try { - for (CardInfo card : newCards) { - cardDao.create(card); - if (classNames != null) { - classNames.add(card.getClassName()); - } - } - } catch (SQLException e) { - Logger.getLogger(CardRepository.class).error("Error adding cards to DB - " + e, e); - processMemoryErrors(e); + cardsDao.callBatchTasks(() -> { + // only add new cards (no updates) + logger.info("DB: need to add " + newCards.size() + " new cards"); + try { + for (CardInfo card : newCards) { + cardsDao.create(card); } + } catch (SQLException e) { + Logger.getLogger(CardRepository.class).error("Error adding cards to DB - " + e, e); + processMemoryErrors(e); } - - // no card updates - return null; }); @@ -158,9 +155,9 @@ public enum CardRepository { public Set getNames() { Set names = new TreeSet<>(); try { - QueryBuilder qb = cardDao.queryBuilder(); + QueryBuilder qb = cardsDao.queryBuilder(); qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName"); - List results = cardDao.query(qb.prepare()); + List results = cardsDao.query(qb.prepare()); for (CardInfo card : results) { addNewNames(card, names); } @@ -174,10 +171,10 @@ public enum CardRepository { public Set getNonLandNames() { Set names = new TreeSet<>(); try { - QueryBuilder qb = cardDao.queryBuilder(); + QueryBuilder qb = cardsDao.queryBuilder(); qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName"); qb.where().not().like("types", new SelectArg('%' + CardType.LAND.name() + '%')); - List results = cardDao.query(qb.prepare()); + List results = cardsDao.query(qb.prepare()); for (CardInfo card : results) { addNewNames(card, names); } @@ -191,14 +188,14 @@ public enum CardRepository { public Set getNonbasicLandNames() { Set names = new TreeSet<>(); try { - QueryBuilder qb = cardDao.queryBuilder(); + QueryBuilder qb = cardsDao.queryBuilder(); qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName"); Where where = qb.where(); where.and( where.not().like("supertypes", '%' + SuperType.BASIC.name() + '%'), where.like("types", '%' + CardType.LAND.name() + '%') ); - List results = cardDao.query(qb.prepare()); + List results = cardsDao.query(qb.prepare()); for (CardInfo card : results) { addNewNames(card, names); } @@ -212,10 +209,10 @@ public enum CardRepository { public Set getNotBasicLandNames() { Set names = new TreeSet<>(); try { - QueryBuilder qb = cardDao.queryBuilder(); + QueryBuilder qb = cardsDao.queryBuilder(); qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName"); qb.where().not().like("supertypes", new SelectArg('%' + SuperType.BASIC.name() + '%')); - List results = cardDao.query(qb.prepare()); + List results = cardsDao.query(qb.prepare()); for (CardInfo card : results) { addNewNames(card, names); } @@ -229,10 +226,10 @@ public enum CardRepository { public Set getCreatureNames() { Set names = new TreeSet<>(); try { - QueryBuilder qb = cardDao.queryBuilder(); + QueryBuilder qb = cardsDao.queryBuilder(); qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName"); qb.where().like("types", new SelectArg('%' + CardType.CREATURE.name() + '%')); - List results = cardDao.query(qb.prepare()); + List results = cardsDao.query(qb.prepare()); for (CardInfo card : results) { addNewNames(card, names); } @@ -246,10 +243,10 @@ public enum CardRepository { public Set getArtifactNames() { Set names = new TreeSet<>(); try { - QueryBuilder qb = cardDao.queryBuilder(); + QueryBuilder qb = cardsDao.queryBuilder(); qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName"); qb.where().like("types", new SelectArg('%' + CardType.ARTIFACT.name() + '%')); - List results = cardDao.query(qb.prepare()); + List results = cardsDao.query(qb.prepare()); for (CardInfo card : results) { addNewNames(card, names); } @@ -263,14 +260,14 @@ public enum CardRepository { public Set getNonLandAndNonCreatureNames() { Set names = new TreeSet<>(); try { - QueryBuilder qb = cardDao.queryBuilder(); + QueryBuilder qb = cardsDao.queryBuilder(); qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName"); Where where = qb.where(); where.and( where.not().like("types", '%' + CardType.CREATURE.name() + '%'), where.not().like("types", '%' + CardType.LAND.name() + '%') ); - List results = cardDao.query(qb.prepare()); + List results = cardsDao.query(qb.prepare()); for (CardInfo card : results) { addNewNames(card, names); } @@ -284,14 +281,14 @@ public enum CardRepository { public Set getNonArtifactAndNonLandNames() { Set names = new TreeSet<>(); try { - QueryBuilder qb = cardDao.queryBuilder(); + QueryBuilder qb = cardsDao.queryBuilder(); qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName"); Where where = qb.where(); where.and( where.not().like("types", '%' + CardType.ARTIFACT.name() + '%'), where.not().like("types", '%' + CardType.LAND.name() + '%') ); - List results = cardDao.query(qb.prepare()); + List results = cardsDao.query(qb.prepare()); for (CardInfo card : results) { addNewNames(card, names); } @@ -308,7 +305,7 @@ public enum CardRepository { public CardInfo findCard(String setCode, String cardNumber, boolean ignoreNightCards) { try { - QueryBuilder queryBuilder = cardDao.queryBuilder(); + QueryBuilder queryBuilder = cardsDao.queryBuilder(); if (ignoreNightCards) { queryBuilder.limit(1L).where() .eq("setCode", new SelectArg(setCode)) @@ -323,7 +320,7 @@ public enum CardRepository { // (example: vow - 65 - Jacob Hauken, Inspector), so make priority for main side first queryBuilder.orderBy("nightCard", true); } - List result = cardDao.query(queryBuilder.prepare()); + List result = cardsDao.query(queryBuilder.prepare()); if (!result.isEmpty()) { return result.get(0); } @@ -337,7 +334,7 @@ public enum CardRepository { public List getClassNames() { List names = new ArrayList<>(); try { - List results = cardDao.queryForAll(); + List results = cardsDao.queryForAll(); for (CardInfo card : results) { names.add(card.getClassName()); } @@ -350,10 +347,10 @@ public enum CardRepository { public List getMissingCards(List classNames) { try { - QueryBuilder queryBuilder = cardDao.queryBuilder(); + QueryBuilder queryBuilder = cardsDao.queryBuilder(); queryBuilder.where().not().in("className", classNames); - return cardDao.query(queryBuilder.prepare()); + return cardsDao.query(queryBuilder.prepare()); } catch (SQLException e) { Logger.getLogger(CardRepository.class).error("Error getting missing cards from DB: " + e, e); processMemoryErrors(e); @@ -424,12 +421,12 @@ public enum CardRepository { * Used for building cubes, packs, and for ensuring that dual faces and split cards have sides/halves from * the same set and variant art. * - * @param name name of the card, or side of the card, to find - * @param expansion the set name from which to find the card - * @param cardNumber the card number for variant arts in one set - * @param returnSplitCardHalf whether to return a half of a split card or the corresponding full card. - * Want this `false` when user is searching by either names in a split card so that - * the full card can be found by either name. + * @param name name of the card, or side of the card, to find + * @param expansion the set name from which to find the card + * @param cardNumber the card number for variant arts in one set + * @param returnSplitCardHalf whether to return a half of a split card or the corresponding full card. + * Want this `false` when user is searching by either names in a split card so that + * the full card can be found by either name. * @return */ public CardInfo findCardWithPreferredSetAndNumber(String name, String expansion, String cardNumber, boolean returnSplitCardHalf) { @@ -461,27 +458,27 @@ public enum CardRepository { * Find a card's reprints from all sets. * It allows for cards to be searched by their full name, or in the case of multi-name cards of the type "A // B" * To search for them using "A", "B", or "A // B". - * + *

* Note of how the function works: - * Out of all card types (Split, MDFC, Adventure, Flip, Transform) - * ONLY Split cards (Fire // Ice) MUST be queried in the DB by the full name when querying by "name". - * Searching for it by either half will return an incorrect result. - * ALL the others MUST be queried for by the first half of their full name (i.e. "A" from "A // B") - * when querying by "name". + * Out of all card types (Split, MDFC, Adventure, Flip, Transform) + * ONLY Split cards (Fire // Ice) MUST be queried in the DB by the full name when querying by "name". + * Searching for it by either half will return an incorrect result. + * ALL the others MUST be queried for by the first half of their full name (i.e. "A" from "A // B") + * when querying by "name". * - * @param name the name of the card to search for - * @param limitByMaxAmount return max amount of different cards (if 0 then return card from all sets) - * @param returnSplitCardHalf whether to return a half of a split card or the corresponding full card. - * Want this `false` when user is searching by either names in a split card so that - * the full card can be found by either name. - * Want this `true` when the client is searching for info on both halves to display it. - * @canCheckDatabaseHealth try to fix database on any errors (use true anytime except fix methods itself) - * @return a list of the reprints of the card if it was found (up to limitByMaxAmount number), - * or an empty list if the card was not found. + * @param name the name of the card to search for + * @param limitByMaxAmount return max amount of different cards (if 0 then return card from all sets) + * @param returnSplitCardHalf whether to return a half of a split card or the corresponding full card. + * Want this `false` when user is searching by either names in a split card so that + * the full card can be found by either name. + * Want this `true` when the client is searching for info on both halves to display it. + * @return a list of the reprints of the card if it was found (up to limitByMaxAmount number), + * or an empty list if the card was not found. + * @canCheckDatabaseHealth try to fix database on any errors (use true anytime except fix methods itself) */ public List findCards(String name, long limitByMaxAmount, boolean returnSplitCardHalf, boolean canCheckDatabaseHealth) { List results; - QueryBuilder queryBuilder = cardDao.queryBuilder(); + QueryBuilder queryBuilder = cardsDao.queryBuilder(); if (limitByMaxAmount > 0) { queryBuilder.limit(limitByMaxAmount); } @@ -492,27 +489,27 @@ public enum CardRepository { // Could be made faster by searching assuming it's NOT a split card and first searching by the first // half of the name, but this is easier to understand. queryBuilder.where().eq("name", new SelectArg(name)); - results = cardDao.query(queryBuilder.prepare()); + results = cardsDao.query(queryBuilder.prepare()); // Result comes back empty, try to search using the first half (could be Adventure, MDFC, etc.) if (results.isEmpty()) { String mainCardName = name.split(" // ", 2)[0]; queryBuilder.where().eq("name", new SelectArg(mainCardName)); - results = cardDao.query(queryBuilder.prepare()); // If still empty, then card can't be found + results = cardsDao.query(queryBuilder.prepare()); // If still empty, then card can't be found } } else { // Cannot tell if string represents the full name of a card or only part of it. // Assume it is the full card name queryBuilder.where().eq("name", new SelectArg(name)); - results = cardDao.query(queryBuilder.prepare()); + results = cardsDao.query(queryBuilder.prepare()); if (results.isEmpty()) { // Nothing found when looking for main name, try looking under the other names queryBuilder.where() - .eq("flipCardName", new SelectArg(name)).or() - .eq("secondSideName", new SelectArg(name)).or() - .eq("adventureSpellName", new SelectArg(name)).or() - .eq("modalDoubleFacedSecondSideName", new SelectArg(name)); - results = cardDao.query(queryBuilder.prepare()); + .eq("flipCardName", new SelectArg(name)).or() + .eq("secondSideName", new SelectArg(name)).or() + .eq("adventureSpellName", new SelectArg(name)).or() + .eq("modalDoubleFacedSecondSideName", new SelectArg(name)); + results = cardsDao.query(queryBuilder.prepare()); } else { // Check that a full card was found and not a SplitCardHalf // Can be caused by searching for "Fire" instead of "Fire // Ice" @@ -522,7 +519,7 @@ public enum CardRepository { queryBuilder.where() .eq("setCode", new SelectArg(firstCardInfo.setCode)).and() .eq("cardNumber", new SelectArg(firstCardInfo.cardNumber)); - List tmpResults = cardDao.query(queryBuilder.prepare()); + List tmpResults = cardsDao.query(queryBuilder.prepare()); String fullSplitCardName = null; for (CardInfo cardInfo : tmpResults) { @@ -536,7 +533,7 @@ public enum CardRepository { } queryBuilder.where().eq("name", new SelectArg(fullSplitCardName)); - results = cardDao.query(queryBuilder.prepare()); + results = cardsDao.query(queryBuilder.prepare()); } } } @@ -557,9 +554,9 @@ public enum CardRepository { public List findCardsByClass(String canonicalClassName) { try { - QueryBuilder queryBuilder = cardDao.queryBuilder(); + QueryBuilder queryBuilder = cardsDao.queryBuilder(); queryBuilder.where().eq("className", new SelectArg(canonicalClassName)); - return cardDao.query(queryBuilder.prepare()); + return cardsDao.query(queryBuilder.prepare()); } catch (SQLException e) { Logger.getLogger(CardRepository.class).error("Error during execution of raw sql statement" + e, e); processMemoryErrors(e); @@ -570,7 +567,7 @@ public enum CardRepository { /** * Warning, don't use db functions in card's code - it generates heavy db loading in AI simulations. If you * need that feature then check for simulation mode. See https://github.com/magefree/mage/issues/7014 - * + *

* Ignoring night cards by default * * @param criteria @@ -578,10 +575,10 @@ public enum CardRepository { */ public List findCards(CardCriteria criteria) { try { - QueryBuilder queryBuilder = cardDao.queryBuilder(); + QueryBuilder queryBuilder = cardsDao.queryBuilder(); criteria.buildQuery(queryBuilder); - return cardDao.query(queryBuilder.prepare()); + return cardsDao.query(queryBuilder.prepare()); } catch (SQLException e) { Logger.getLogger(CardRepository.class).error("Error during execution of card repository query statement: " + e, e); processMemoryErrors(e); @@ -622,7 +619,7 @@ public enum CardRepository { public long getContentVersionFromDB() { try { - ConnectionSource connectionSource = new JdbcConnectionSource(JDBC_URL); + ConnectionSource connectionSource = new JdbcConnectionSource(DatabaseUtils.prepareH2Connection(DatabaseUtils.DB_NAME_CARDS, false)); return RepositoryUtil.getDatabaseVersion(connectionSource, VERSION_ENTITY_NAME + "Content"); } catch (SQLException e) { Logger.getLogger(CardRepository.class).error("Error getting content version from DB - " + e, e); @@ -633,7 +630,7 @@ public enum CardRepository { public void setContentVersion(long version) { try { - ConnectionSource connectionSource = new JdbcConnectionSource(JDBC_URL); + ConnectionSource connectionSource = new JdbcConnectionSource(DatabaseUtils.prepareH2Connection(DatabaseUtils.DB_NAME_CARDS, false)); RepositoryUtil.updateVersion(connectionSource, VERSION_ENTITY_NAME + "Content", version); } catch (SQLException e) { Logger.getLogger(CardRepository.class).error("Error setting content version - " + e, e); @@ -645,25 +642,84 @@ public enum CardRepository { return CARD_CONTENT_VERSION; } - public void closeDB() { + public void closeDB(boolean writeCompact) { try { - if (cardDao != null && cardDao.getConnectionSource() != null) { - DatabaseConnection conn = cardDao.getConnectionSource().getReadWriteConnection(cardDao.getTableName()); - conn.executeStatement("shutdown compact", 0); + if (cardsDao != null && cardsDao.getConnectionSource() != null) { + DatabaseConnection conn = cardsDao.getConnectionSource().getReadWriteConnection(cardsDao.getTableName()); + if (writeCompact) { + conn.executeStatement("SHUTDOWN COMPACT", 0); // compact data and rewrite whole db + } else { + conn.executeStatement("SHUTDOWN IMMEDIATELY", 0); // close without any writes + } + cardsDao.getConnectionSource().releaseConnection(conn); } } catch (SQLException ignore) { } } - private void openDB() { + public void openDB() { try { - ConnectionSource connectionSource = new JdbcConnectionSource(JDBC_URL); - cardDao = DaoManager.createDao(connectionSource, CardInfo.class); + ConnectionSource connectionSource = new JdbcConnectionSource(DatabaseUtils.prepareH2Connection(DatabaseUtils.DB_NAME_CARDS, true)); + cardsDao = DaoManager.createDao(connectionSource, CardInfo.class); } catch (SQLException e) { Logger.getLogger(CardRepository.class).error("Error opening card repository - " + e, e); } } + public void printDatabaseStats(String info) { + List> allSettings = querySQL("SELECT NAME, VALUE FROM INFORMATION_SCHEMA.SETTINGS"); + if (allSettings == null) { + return; + } + + // cache + logger.info("Database cache settings (" + info + "):"); + allSettings.stream().filter(values -> values.get(0).equals("CACHE_SIZE")).forEach(values -> { + logger.info(" - cache size, setup: " + values.get(1) + " kb"); + }); + allSettings.stream().filter(values -> values.get(0).equals("info.CACHE_MAX_SIZE")).forEach(values -> { + logger.info(" - cache size, max: " + values.get(1) + " mb"); + }); + allSettings.stream().filter(values -> values.get(0).equals("info.CACHE_SIZE")).forEach(values -> { + logger.info(" - cache size, current: " + values.get(1) + " mb"); + }); + + // memory + allSettings = querySQL("SELECT MEMORY_FREE(), MEMORY_USED()"); + if (allSettings == null) { + return; + } + logger.info("Database memory stats (" + info + "):"); + logger.info(" - free: " + allSettings.get(0).get(0) + " kb"); + logger.info(" - used: " + allSettings.get(0).get(1) + " kb"); + } + + /** + * Exec any SQL query and return result table as string values + */ + public List> querySQL(String sql) { + try { + GenericRawResults query = cardsDao.queryRaw(sql); + return query.getResults().stream() + .map(Arrays::asList) + .collect(Collectors.toList()); + } catch (SQLException e) { + logger.error("Can't query sql due error: " + sql + " - " + e, e); + return null; + } + } + + /** + * Exec any SQL code without result. Can be used to change db settings like SET xxx = YYY + */ + public void execSQL(String sql) { + try { + cardsDao.executeRaw(sql); + } catch (SQLException e) { + logger.error("Can't exec sql due error: " + sql + " - " + e, e); + } + } + private static CardInfo safeFindKnownCard() { // safe find of known card with memory/db fixes return instance.findCards("Silvercoat Lion", 1, false, false) @@ -690,7 +746,7 @@ public enum CardRepository { } // DB seems to have a problem - try to restart the DB (useless in 99% due out of memory problems) - instance.closeDB(); + instance.closeDB(false); instance.openDB(); cardInfo = safeFindKnownCard(); if (cardInfo != null) { diff --git a/Mage/src/main/java/mage/cards/repository/DatabaseUtils.java b/Mage/src/main/java/mage/cards/repository/DatabaseUtils.java new file mode 100644 index 00000000000..1c94a2a3996 --- /dev/null +++ b/Mage/src/main/java/mage/cards/repository/DatabaseUtils.java @@ -0,0 +1,69 @@ +package mage.cards.repository; + +import mage.util.DebugUtil; + +/** + * Helper class for database + * + * @author JayDi85 + */ +public class DatabaseUtils { + + // warning, do not change names or db format + // h2 + public static final String DB_NAME_FEEDBACK = "feedback.h2"; + public static final String DB_NAME_USERS = "authorized_user.h2"; + public static final String DB_NAME_CARDS = "cards.h2"; + // sqlite (usage reason: h2 database works bad with 1GB+ files and can break it) + public static final String DB_NAME_RECORDS = "table_record.db"; + public static final String DB_NAME_STATS = "user_stats.db"; + + /** + * Prepare JDBC connection string and setup additional params for H2 databases + * + * @param dbName database name like "cards.h2" + * @param improveCaches use memory optimizations for cards database (no needs for other dbs) + */ + public static String prepareH2Connection(String dbName, boolean improveCaches) { + // example: jdbc:h2:file:./db/cards.h2;AUTO_SERVER=TRUE;IGNORECASE=TRUE + String res = String.format("jdbc:h2:file:./db/%s", dbName); + + // shared params + res += ";AUTO_SERVER=TRUE"; // open database in mix mode (first open by new thread, second open by new jvm-process) + res += ";IGNORECASE=TRUE"; // ignore char case for text searching + + // additional params + // can be defined by connection string, by exec sql like "SET xxx = yyy", by settings from existing db-file + + if (improveCaches) { + // CACHE_SIZE + // max query cache size in kb (default: 65 Mb per 1 GB of java's max memory) + // warning, xmage require 150Mb cache for big queries in AI games like all card names (db can be broken on lower cache) + //res += ";CACHE_SIZE=150000"; + res += ";CACHE_SIZE=" + Math.round(Math.max(150000, Runtime.getRuntime().maxMemory() * 0.1 / 1024)); + + + // QUERY_CACHE_SIZE + // queries amount per session to cache (default: 8) + res += ";QUERY_CACHE_SIZE=32"; + } + + // add debug stats (see DebugUtil for usage instruction) + if (DebugUtil.DATABASE_PROFILE_SQL_QUERIES_TO_FILE) { + res += ";TRACE_LEVEL_FILE=2"; + res += ";QUERY_STATISTICS=TRUE"; + } + + return res; + } + + /** + * Prepare JDBC connection string and setup additional params for SQLite databases + * + * @param dbName database name like "cards" + */ + public static String prepareSqliteConnection(String dbName) { + // example: jdbc:sqlite:./db/table_record.db + return String.format("jdbc:sqlite:./db/%s", dbName); + } +} diff --git a/Mage/src/main/java/mage/cards/repository/ExpansionRepository.java b/Mage/src/main/java/mage/cards/repository/ExpansionRepository.java index 9a671424d28..ddee9612d94 100644 --- a/Mage/src/main/java/mage/cards/repository/ExpansionRepository.java +++ b/Mage/src/main/java/mage/cards/repository/ExpansionRepository.java @@ -27,13 +27,13 @@ public enum ExpansionRepository { private static final Logger logger = Logger.getLogger(ExpansionRepository.class); - private static final String JDBC_URL = "jdbc:h2:file:./db/cards.h2;AUTO_SERVER=TRUE;IGNORECASE=TRUE"; + // TODO: delete db version from cards and expansions due un-used (that's dbs re-created on each update) private static final String VERSION_ENTITY_NAME = "expansion"; private static final long EXPANSION_DB_VERSION = 5; private static final long EXPANSION_CONTENT_VERSION = 18; private Dao expansionDao; - private RepositoryEventSource eventSource = new RepositoryEventSource(); + private final RepositoryEventSource eventSource = new RepositoryEventSource(); public boolean instanceInitialized = false; ExpansionRepository() { @@ -42,7 +42,7 @@ public enum ExpansionRepository { file.mkdirs(); } try { - ConnectionSource connectionSource = new JdbcConnectionSource(JDBC_URL); + ConnectionSource connectionSource = new JdbcConnectionSource(DatabaseUtils.prepareH2Connection(DatabaseUtils.DB_NAME_CARDS, true)); boolean isObsolete = RepositoryUtil.isDatabaseObsolete(connectionSource, VERSION_ENTITY_NAME, EXPANSION_DB_VERSION); boolean isNewBuild = RepositoryUtil.isNewBuildRun(connectionSource, VERSION_ENTITY_NAME, ExpansionRepository.class); // recreate db on new build @@ -218,7 +218,7 @@ public enum ExpansionRepository { public long getContentVersionFromDB() { try { - ConnectionSource connectionSource = new JdbcConnectionSource(JDBC_URL); + ConnectionSource connectionSource = new JdbcConnectionSource(DatabaseUtils.prepareH2Connection(DatabaseUtils.DB_NAME_CARDS, false)); return RepositoryUtil.getDatabaseVersion(connectionSource, VERSION_ENTITY_NAME + "Content"); } catch (SQLException ex) { ex.printStackTrace(); @@ -228,7 +228,7 @@ public enum ExpansionRepository { public void setContentVersion(long version) { try { - ConnectionSource connectionSource = new JdbcConnectionSource(JDBC_URL); + ConnectionSource connectionSource = new JdbcConnectionSource(DatabaseUtils.prepareH2Connection(DatabaseUtils.DB_NAME_CARDS, false)); RepositoryUtil.updateVersion(connectionSource, VERSION_ENTITY_NAME + "Content", version); } catch (SQLException e) { logger.error("Error setting content version - " + e, e); diff --git a/Mage/src/main/java/mage/cards/repository/RepositoryEvent.java b/Mage/src/main/java/mage/cards/repository/RepositoryEvent.java index 2056f747199..5e801cb0f34 100644 --- a/Mage/src/main/java/mage/cards/repository/RepositoryEvent.java +++ b/Mage/src/main/java/mage/cards/repository/RepositoryEvent.java @@ -10,6 +10,7 @@ import java.util.EventObject; */ public class RepositoryEvent extends EventObject implements ExternalEvent, Serializable { + // TODO: db changed on update only, events can be deleted public enum RepositoryEventType { DB_LOADED, DB_UPDATED } diff --git a/Mage/src/main/java/mage/cards/repository/RepositoryUtil.java b/Mage/src/main/java/mage/cards/repository/RepositoryUtil.java index 8e049dd8c2d..775d746f115 100644 --- a/Mage/src/main/java/mage/cards/repository/RepositoryUtil.java +++ b/Mage/src/main/java/mage/cards/repository/RepositoryUtil.java @@ -7,6 +7,7 @@ import com.j256.ormlite.stmt.QueryBuilder; import com.j256.ormlite.stmt.SelectArg; import com.j256.ormlite.support.ConnectionSource; import com.j256.ormlite.table.TableUtils; +import mage.util.DebugUtil; import mage.util.JarVersion; import org.apache.log4j.Logger; @@ -39,22 +40,26 @@ public final class RepositoryUtil { logger.info(" - emblems: " + TokenRepository.instance.getByType(TokenType.EMBLEM).size()); logger.info(" - planes: " + TokenRepository.instance.getByType(TokenType.PLANE).size()); logger.info(" - dungeons: " + TokenRepository.instance.getByType(TokenType.DUNGEON).size()); + + if (DebugUtil.DATABASE_SHOW_CACHE_AND_MEMORY_STATS_ON_STARTUP) { + CardRepository.instance.printDatabaseStats("on startup"); + } } public static boolean isDatabaseObsolete(ConnectionSource connectionSource, String entityName, long version) throws SQLException { TableUtils.createTableIfNotExists(connectionSource, DatabaseVersion.class); - Dao dbVersionDao = DaoManager.createDao(connectionSource, DatabaseVersion.class); + Dao versionDao = DaoManager.createDao(connectionSource, DatabaseVersion.class); - QueryBuilder queryBuilder = dbVersionDao.queryBuilder(); + QueryBuilder queryBuilder = versionDao.queryBuilder(); queryBuilder.where().eq("entity", new SelectArg(entityName)) .and().eq("version", new SelectArg(version)); - List dbVersions = dbVersionDao.query(queryBuilder.prepare()); + List dbVersions = versionDao.query(queryBuilder.prepare()); if (dbVersions.isEmpty()) { DatabaseVersion dbVersion = new DatabaseVersion(); dbVersion.setEntity(entityName); dbVersion.setVersion(version); - dbVersionDao.create(dbVersion); + versionDao.create(dbVersion); } return dbVersions.isEmpty(); } @@ -68,48 +73,48 @@ public final class RepositoryUtil { } TableUtils.createTableIfNotExists(connectionSource, DatabaseBuild.class); - Dao dbBuildDao = DaoManager.createDao(connectionSource, DatabaseBuild.class); + Dao buildDao = DaoManager.createDao(connectionSource, DatabaseBuild.class); - QueryBuilder queryBuilder = dbBuildDao.queryBuilder(); + QueryBuilder queryBuilder = buildDao.queryBuilder(); queryBuilder.where().eq("entity", new SelectArg(entityName)) .and().eq("last_build", new SelectArg(currentBuild)); - List dbBuilds = dbBuildDao.query(queryBuilder.prepare()); + List dbBuilds = buildDao.query(queryBuilder.prepare()); if (dbBuilds.isEmpty()) { DatabaseBuild dbBuild = new DatabaseBuild(); dbBuild.setEntity(entityName); dbBuild.setLastBuild(currentBuild); - dbBuildDao.create(dbBuild); + buildDao.create(dbBuild); } return dbBuilds.isEmpty(); } public static void updateVersion(ConnectionSource connectionSource, String entityName, long version) throws SQLException { TableUtils.createTableIfNotExists(connectionSource, DatabaseVersion.class); - Dao dbVersionDao = DaoManager.createDao(connectionSource, DatabaseVersion.class); + Dao versionDao = DaoManager.createDao(connectionSource, DatabaseVersion.class); - QueryBuilder queryBuilder = dbVersionDao.queryBuilder(); + QueryBuilder queryBuilder = versionDao.queryBuilder(); queryBuilder.where().eq("entity", new SelectArg(entityName)); - List dbVersions = dbVersionDao.query(queryBuilder.prepare()); + List dbVersions = versionDao.query(queryBuilder.prepare()); if (!dbVersions.isEmpty()) { - DeleteBuilder deleteBuilder = dbVersionDao.deleteBuilder(); + DeleteBuilder deleteBuilder = versionDao.deleteBuilder(); deleteBuilder.where().eq("entity", new SelectArg(entityName)); deleteBuilder.delete(); } DatabaseVersion databaseVersion = new DatabaseVersion(); databaseVersion.setEntity(entityName); databaseVersion.setVersion(version); - dbVersionDao.create(databaseVersion); + versionDao.create(databaseVersion); } public static long getDatabaseVersion(ConnectionSource connectionSource, String entityName) throws SQLException { TableUtils.createTableIfNotExists(connectionSource, DatabaseVersion.class); - Dao dbVersionDao = DaoManager.createDao(connectionSource, DatabaseVersion.class); + Dao versionDao = DaoManager.createDao(connectionSource, DatabaseVersion.class); - QueryBuilder queryBuilder = dbVersionDao.queryBuilder(); + QueryBuilder queryBuilder = versionDao.queryBuilder(); queryBuilder.where().eq("entity", new SelectArg(entityName)); - List dbVersions = dbVersionDao.query(queryBuilder.prepare()); + List dbVersions = versionDao.query(queryBuilder.prepare()); if (dbVersions.isEmpty()) { return 0; } else { diff --git a/Mage/src/main/java/mage/util/DebugUtil.java b/Mage/src/main/java/mage/util/DebugUtil.java index f599699165f..836312f0e0c 100644 --- a/Mage/src/main/java/mage/util/DebugUtil.java +++ b/Mage/src/main/java/mage/util/DebugUtil.java @@ -39,6 +39,18 @@ public class DebugUtil { // game dialogs public static boolean GUI_GAME_DIALOGS_DRAW_CARDS_AREA_BORDER = false; + // database - show additional info about cache and memory settings + public static boolean DATABASE_SHOW_CACHE_AND_MEMORY_STATS_ON_STARTUP = true; + + // database - collect sql queries and stats + // how-to use: + // - clean db folders or delete all *.trace.db files + // - run tests or real server to collect some stats + // - download h2 files for ver 1.4.197 from https://h2database.com/h2-2018-03-18.zip and open tools folder like xxx\H2\bin + // - execute command: java -cp "h2-1.4.196.jar;%H2DRIVERS%;%CLASSPATH%" org.h2.tools.ConvertTraceFile -traceFile "xxx\Mage.Tests\db\cards.h2.trace.db" -script "xxx\Mage.Tests\db\cards.h2.trace.sql" + // - open *.sql file for all sql-queries and exec stats + public static boolean DATABASE_PROFILE_SQL_QUERIES_TO_FILE = false; + public static String getMethodNameWithSource(final int depth) { return TraceHelper.getMethodNameWithSource(depth); }