server: database improves:

- fixed broken database in some use cases (example: AI and choose name dialog, related to #11285);
- added docs and debug tools for sql queries, caches and memory analyse (see DebugUtil);
- refactor code to use shared settings;
- deleted outdated and un-used code (db logs, stats, etc);
This commit is contained in:
Oleg Agafonov 2024-07-18 21:22:10 +04:00
parent c448612c97
commit bf3f26ccc1
18 changed files with 376 additions and 394 deletions

View file

@ -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);

View file

@ -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<Log, Object> logDao;
private Dao<Feedback, Object> 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<Log> getAllLogs() {
List<Log> 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);

View file

@ -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<Log> 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<Feedback> feedbackList = EntityManager.instance.getAllFeedbacks();
System.out.println("feedbacks found: " + feedbackList.size());
int count = 1;

View file

@ -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<Log> logs = EntityManager.instance.getAllLogs();
System.out.println("logs found: " + logs.size());
Map<String, Integer> nicknames = displayCommonNumbers(logs);
List<Integer> games = displayTop3(nicknames);
displayPlayedOnlyOnce(games);
System.out.println("Done");
}
private static void displayPlayedOnlyOnce(List<Integer> 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<Integer> displayTop3(Map<String, Integer> nicknames) {
Collection<Integer> values = nicknames.values();
List<Integer> games = new ArrayList<>();
games.addAll(values);
Collections.sort(games, new Comparator<Integer>() {
@Override
public int compare(Integer i1, Integer i2) {
return i2.compareTo(i1);
}
});
// Top-3
List<Integer> numbersToFind = new ArrayList<>();
for (Integer numberOfGames : games) {
numbersToFind.add(numberOfGames);
if (numbersToFind.size() == 3) {
break;
}
}
Map<Integer, String> players = new LinkedHashMap<>();
for (Map.Entry<String, Integer> 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<Integer, String> entry : players.entrySet()) {
System.out.println(" " + entry.getValue() + ": " + entry.getKey());
}
return games;
}
private static Map<String, Integer> displayCommonNumbers(List<Log> logs) {
int count = 0;
Map<String, Integer> 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<String, Integer> 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<Integer> numbers, Integer value) {
for (Integer number : numbers) {
if (number.equals(value)) {
return true;
}
}
return false;
}
}

View file

@ -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;
}
}

View file

@ -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";
}

View file

@ -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<AuthorizedUser, Object> dao;
private Dao<AuthorizedUser, Object> 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<AuthorizedUser, Object> db = dao.deleteBuilder();
DeleteBuilder<AuthorizedUser, Object> 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<AuthorizedUser, Object> qb = dao.queryBuilder();
QueryBuilder<AuthorizedUser, Object> qb = usersDao.queryBuilder();
qb.where().eq("name", new SelectArg(userName));
List<AuthorizedUser> results = dao.query(qb.prepare());
List<AuthorizedUser> 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<AuthorizedUser, Object> qb = dao.queryBuilder();
QueryBuilder<AuthorizedUser, Object> qb = usersDao.queryBuilder();
qb.where().eq("email", new SelectArg(userName));
List<AuthorizedUser> results = dao.query(qb.prepare());
List<AuthorizedUser> 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) {

View file

@ -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<TableRecord, Object> dao;
private Dao<TableRecord, Object> 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<TableRecord> getAfter(long endTimeMs) {
try {
QueryBuilder<TableRecord, Object> qb = dao.queryBuilder();
QueryBuilder<TableRecord, Object> 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);

View file

@ -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<UserStats, Object> dao;
private Dao<UserStats, Object> 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<UserStats, Object> qb = dao.queryBuilder();
QueryBuilder<UserStats, Object> qb = statsDao.queryBuilder();
qb.limit(1L).where().eq("userName", new SelectArg(userName));
List<UserStats> users = dao.query(qb.prepare());
List<UserStats> users = statsDao.query(qb.prepare());
if (!users.isEmpty()) {
return users.get(0);
}
@ -81,8 +81,8 @@ public enum UserStatsRepository {
public List<UserStats> getAllUsers() {
try {
QueryBuilder<UserStats, Object> qb = dao.queryBuilder();
return dao.query(qb.prepare());
QueryBuilder<UserStats, Object> 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<UserStats, Object> qb = dao.queryBuilder();
QueryBuilder<UserStats, Object> qb = statsDao.queryBuilder();
qb.orderBy("endTimeMs", false).limit(1L);
List<UserStats> users = dao.query(qb.prepare());
List<UserStats> 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);

View file

@ -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();
}
}

View file

@ -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"));

View file

@ -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<CardInfo, Object> cardDao;
private Set<String> 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<CardInfo, Object> cardsDao;
// sets with exclusively snow basics
public static final Set<String> 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<CardInfo> newCards, long newContentVersion) {
if (newCards == null || newCards.isEmpty()) {
return;
}
try {
cardDao.callBatchTasks(() -> {
// add
if (newCards != null && !newCards.isEmpty()) {
cardsDao.callBatchTasks(() -> {
// only add new cards (no updates)
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());
}
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<String> getNames() {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
List<CardInfo> results = cardDao.query(qb.prepare());
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
addNewNames(card, names);
}
@ -174,10 +171,10 @@ public enum CardRepository {
public Set<String> getNonLandNames() {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
qb.where().not().like("types", new SelectArg('%' + CardType.LAND.name() + '%'));
List<CardInfo> results = cardDao.query(qb.prepare());
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
addNewNames(card, names);
}
@ -191,14 +188,14 @@ public enum CardRepository {
public Set<String> getNonbasicLandNames() {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
Where<CardInfo, Object> where = qb.where();
where.and(
where.not().like("supertypes", '%' + SuperType.BASIC.name() + '%'),
where.like("types", '%' + CardType.LAND.name() + '%')
);
List<CardInfo> results = cardDao.query(qb.prepare());
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
addNewNames(card, names);
}
@ -212,10 +209,10 @@ public enum CardRepository {
public Set<String> getNotBasicLandNames() {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
qb.where().not().like("supertypes", new SelectArg('%' + SuperType.BASIC.name() + '%'));
List<CardInfo> results = cardDao.query(qb.prepare());
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
addNewNames(card, names);
}
@ -229,10 +226,10 @@ public enum CardRepository {
public Set<String> getCreatureNames() {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
qb.where().like("types", new SelectArg('%' + CardType.CREATURE.name() + '%'));
List<CardInfo> results = cardDao.query(qb.prepare());
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
addNewNames(card, names);
}
@ -246,10 +243,10 @@ public enum CardRepository {
public Set<String> getArtifactNames() {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
qb.where().like("types", new SelectArg('%' + CardType.ARTIFACT.name() + '%'));
List<CardInfo> results = cardDao.query(qb.prepare());
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
addNewNames(card, names);
}
@ -263,14 +260,14 @@ public enum CardRepository {
public Set<String> getNonLandAndNonCreatureNames() {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
Where<CardInfo, Object> where = qb.where();
where.and(
where.not().like("types", '%' + CardType.CREATURE.name() + '%'),
where.not().like("types", '%' + CardType.LAND.name() + '%')
);
List<CardInfo> results = cardDao.query(qb.prepare());
List<CardInfo> results = cardsDao.query(qb.prepare());
for (CardInfo card : results) {
addNewNames(card, names);
}
@ -284,14 +281,14 @@ public enum CardRepository {
public Set<String> getNonArtifactAndNonLandNames() {
Set<String> names = new TreeSet<>();
try {
QueryBuilder<CardInfo, Object> qb = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> qb = cardsDao.queryBuilder();
qb.distinct().selectColumns("name", "modalDoubleFacedSecondSideName", "secondSideName", "flipCardName");
Where<CardInfo, Object> where = qb.where();
where.and(
where.not().like("types", '%' + CardType.ARTIFACT.name() + '%'),
where.not().like("types", '%' + CardType.LAND.name() + '%')
);
List<CardInfo> results = cardDao.query(qb.prepare());
List<CardInfo> 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<CardInfo, Object> queryBuilder = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> 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<CardInfo> result = cardDao.query(queryBuilder.prepare());
List<CardInfo> result = cardsDao.query(queryBuilder.prepare());
if (!result.isEmpty()) {
return result.get(0);
}
@ -337,7 +334,7 @@ public enum CardRepository {
public List<String> getClassNames() {
List<String> names = new ArrayList<>();
try {
List<CardInfo> results = cardDao.queryForAll();
List<CardInfo> results = cardsDao.queryForAll();
for (CardInfo card : results) {
names.add(card.getClassName());
}
@ -350,10 +347,10 @@ public enum CardRepository {
public List<CardInfo> getMissingCards(List<String> classNames) {
try {
QueryBuilder<CardInfo, Object> queryBuilder = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> 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);
@ -461,7 +458,7 @@ 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".
*
* <p>
* 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".
@ -475,13 +472,13 @@ public enum CardRepository {
* 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.
* @canCheckDatabaseHealth try to fix database on any errors (use true anytime except fix methods itself)
*/
public List<CardInfo> findCards(String name, long limitByMaxAmount, boolean returnSplitCardHalf, boolean canCheckDatabaseHealth) {
List<CardInfo> results;
QueryBuilder<CardInfo, Object> queryBuilder = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> queryBuilder = cardsDao.queryBuilder();
if (limitByMaxAmount > 0) {
queryBuilder.limit(limitByMaxAmount);
}
@ -492,18 +489,18 @@ 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
@ -512,7 +509,7 @@ public enum CardRepository {
.eq("secondSideName", new SelectArg(name)).or()
.eq("adventureSpellName", new SelectArg(name)).or()
.eq("modalDoubleFacedSecondSideName", new SelectArg(name));
results = cardDao.query(queryBuilder.prepare());
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<CardInfo> tmpResults = cardDao.query(queryBuilder.prepare());
List<CardInfo> 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<CardInfo> findCardsByClass(String canonicalClassName) {
try {
QueryBuilder<CardInfo, Object> queryBuilder = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> 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
*
* <p>
* Ignoring night cards by default
*
* @param criteria
@ -578,10 +575,10 @@ public enum CardRepository {
*/
public List<CardInfo> findCards(CardCriteria criteria) {
try {
QueryBuilder<CardInfo, Object> queryBuilder = cardDao.queryBuilder();
QueryBuilder<CardInfo, Object> 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<List<String>> 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<List<String>> querySQL(String sql) {
try {
GenericRawResults<String[]> 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) {

View file

@ -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);
}
}

View file

@ -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<ExpansionInfo, Object> 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);

View file

@ -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
}

View file

@ -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<DatabaseVersion, Object> dbVersionDao = DaoManager.createDao(connectionSource, DatabaseVersion.class);
Dao<DatabaseVersion, Object> versionDao = DaoManager.createDao(connectionSource, DatabaseVersion.class);
QueryBuilder<DatabaseVersion, Object> queryBuilder = dbVersionDao.queryBuilder();
QueryBuilder<DatabaseVersion, Object> queryBuilder = versionDao.queryBuilder();
queryBuilder.where().eq("entity", new SelectArg(entityName))
.and().eq("version", new SelectArg(version));
List<DatabaseVersion> dbVersions = dbVersionDao.query(queryBuilder.prepare());
List<DatabaseVersion> 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<DatabaseBuild, Object> dbBuildDao = DaoManager.createDao(connectionSource, DatabaseBuild.class);
Dao<DatabaseBuild, Object> buildDao = DaoManager.createDao(connectionSource, DatabaseBuild.class);
QueryBuilder<DatabaseBuild, Object> queryBuilder = dbBuildDao.queryBuilder();
QueryBuilder<DatabaseBuild, Object> queryBuilder = buildDao.queryBuilder();
queryBuilder.where().eq("entity", new SelectArg(entityName))
.and().eq("last_build", new SelectArg(currentBuild));
List<DatabaseBuild> dbBuilds = dbBuildDao.query(queryBuilder.prepare());
List<DatabaseBuild> 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<DatabaseVersion, Object> dbVersionDao = DaoManager.createDao(connectionSource, DatabaseVersion.class);
Dao<DatabaseVersion, Object> versionDao = DaoManager.createDao(connectionSource, DatabaseVersion.class);
QueryBuilder<DatabaseVersion, Object> queryBuilder = dbVersionDao.queryBuilder();
QueryBuilder<DatabaseVersion, Object> queryBuilder = versionDao.queryBuilder();
queryBuilder.where().eq("entity", new SelectArg(entityName));
List<DatabaseVersion> dbVersions = dbVersionDao.query(queryBuilder.prepare());
List<DatabaseVersion> dbVersions = versionDao.query(queryBuilder.prepare());
if (!dbVersions.isEmpty()) {
DeleteBuilder<DatabaseVersion, Object> deleteBuilder = dbVersionDao.deleteBuilder();
DeleteBuilder<DatabaseVersion, Object> 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<DatabaseVersion, Object> dbVersionDao = DaoManager.createDao(connectionSource, DatabaseVersion.class);
Dao<DatabaseVersion, Object> versionDao = DaoManager.createDao(connectionSource, DatabaseVersion.class);
QueryBuilder<DatabaseVersion, Object> queryBuilder = dbVersionDao.queryBuilder();
QueryBuilder<DatabaseVersion, Object> queryBuilder = versionDao.queryBuilder();
queryBuilder.where().eq("entity", new SelectArg(entityName));
List<DatabaseVersion> dbVersions = dbVersionDao.query(queryBuilder.prepare());
List<DatabaseVersion> dbVersions = versionDao.query(queryBuilder.prepare());
if (dbVersions.isEmpty()) {
return 0;
} else {

View file

@ -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);
}