Replace repo query with queryDictionary and improve auto update error checking

This commit is contained in:
James Seibel
2023-10-02 21:24:41 -05:00
parent 1ec06fa94e
commit d1df845e79
4 changed files with 103 additions and 73 deletions
@@ -25,6 +25,7 @@ import org.jetbrains.annotations.Nullable;
import org.junit.Assert;
import java.sql.*;
import java.util.*;
/**
* Handles interfacing with SQL databases.
@@ -70,19 +71,15 @@ public abstract class AbstractDhRepo<TDTO extends IBaseDTO>
public TDTO get(TDTO dto) { return this.getByPrimaryKey(dto.getPrimaryKeyString()); }
public TDTO getByPrimaryKey(String primaryKey)
{
TDTO dto = null;
try
Map<String, Object> objectMap = this.queryDictionaryFirst(this.createSelectPrimaryKeySql(primaryKey));
if (objectMap != null && !objectMap.isEmpty())
{
ResultSet resultSet = this.query(this.createSelectPrimaryKeySql(primaryKey));
dto = this.convertResultSetToDto(resultSet);
resultSet.close();
return this.convertDictionaryToDto(objectMap);
}
catch (SQLException e)
else
{
System.err.println(e.getMessage());
return null;
}
return dto;
}
public void save(TDTO dto)
@@ -96,53 +93,56 @@ public abstract class AbstractDhRepo<TDTO extends IBaseDTO>
this.insert(dto);
}
}
private void insert(TDTO dto) { this.queryNoResult(this.createInsertSql(dto)); }
private void update(TDTO dto) { this.queryNoResult(this.createUpdateSql(dto)); }
private void insert(TDTO dto) { this.queryDictionaryFirst(this.createInsertSql(dto)); }
private void update(TDTO dto) { this.queryDictionaryFirst(this.createUpdateSql(dto)); }
public void delete(TDTO dto) { this.queryDictionaryFirst(this.createDeleteSql(dto)); }
public void delete(TDTO dto) { this.queryNoResult(this.createDeleteSql(dto)); }
//==============//
// low level DB //
//==============//
public void queryNoResult(String sql) { this.query(sql, false); }
public ResultSet query(String sql) { return this.query(sql, true); }
public List<Map<String, Object>> queryDictionary(String sql) { return this.query(sql); }
@Nullable
public Map<String, Object> queryDictionaryFirst(String sql)
{
List<Map<String, Object>> objectList = this.query(sql);
return (objectList != null && !objectList.isEmpty()) ? objectList.get(0) : null;
}
/** note: this can only handle 1 command at a time */
@Nullable
private ResultSet query(String sql, boolean returnResultSet) throws RuntimeException
private List<Map<String, Object>> query(String sql) throws RuntimeException
{
try
try (Statement statement = this.connection.createStatement())
{
Statement statement = this.connection.createStatement();
statement.setQueryTimeout(TIMEOUT_SECONDS);
// Note: this can only handle 1 command at a time
boolean resultSetPresent = statement.execute(sql);
ResultSet resultSet = statement.getResultSet();
if (resultSetPresent)
{
ResultSet resultSet = statement.getResultSet();
if (returnResultSet)
{
return resultSet;
}
else
{
resultSet.close();
return null;
}
List<Map<String, Object>> resultList = convertResultSetToDictionaryList(resultSet);
resultSet.close();
return resultList;
}
else
{
return null;
if (resultSet != null)
{
resultSet.close();
}
return new ArrayList<>();
}
}
catch(SQLException e)
{
// SQL exceptions generally only happen when something is wrong with
// the database or the query and should cause the system to blow up to notify the developer
throw new RuntimeException(e);
throw new RuntimeException("Unexpected Query error: ["+e.getMessage()+"], for script: ["+sql+"].", e);
}
}
@@ -189,6 +189,35 @@ public abstract class AbstractDhRepo<TDTO extends IBaseDTO>
public String createWherePrimaryKeyStatement(TDTO dto) { return this.createWherePrimaryKeyStatement(dto.getPrimaryKeyString()); }
public String createWherePrimaryKeyStatement(String primaryKeyValue) { return "WHERE "+this.getPrimaryKeyName()+" = '"+primaryKeyValue+"'"; }
public static List<Map<String, Object>> convertResultSetToDictionaryList(ResultSet resultSet) throws SQLException
{
List<Map<String, Object>> list = new ArrayList<>();
ResultSetMetaData resultMetaData = resultSet.getMetaData();
int resultColumnCount = resultMetaData.getColumnCount();
while (resultSet.next())
{
HashMap<String, Object> object = new HashMap<>();
for (int columnIndex = 1; columnIndex <= resultColumnCount; columnIndex++) // column indices start at 1
{
String columnName = resultMetaData.getColumnName(columnIndex);
if (columnName == null || columnName.equals(""))
{
throw new RuntimeException("SQL result set is missing a column name for column ["+resultMetaData.getTableName(columnIndex)+"."+columnIndex+"].");
}
Object columnValue = resultSet.getObject(columnIndex);
object.put(columnName, columnValue);
}
list.add(object);
}
return list;
}
//==================//
@@ -199,7 +228,7 @@ public abstract class AbstractDhRepo<TDTO extends IBaseDTO>
public abstract String getPrimaryKeyName();
@Nullable
public abstract TDTO convertResultSetToDto(ResultSet resultSet) throws SQLException;
public abstract TDTO convertDictionaryToDto(Map<String, Object> objectMap) throws ClassCastException;
public abstract String createSelectPrimaryKeySql(String primaryKey);
@@ -25,9 +25,9 @@ import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.net.URISyntaxException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Map;
public class DatabaseUpdater
{
@@ -48,40 +48,48 @@ public class DatabaseUpdater
}
catch (URISyntaxException | IOException e)
{
// shouldn't normally happen, but just incase
// shouldn't normally happen, but just in case
throw new RuntimeException(e);
}
// create the base update table if necessary
ResultSet schemaTableExistsResult = repo.query("SELECT COUNT(name) FROM sqlite_master WHERE type='table' AND name='Schema';");
if (schemaTableExistsResult.next())
Map<String, Object> schemaTableExistsResult = repo.queryDictionaryFirst("SELECT COUNT(name) as 'tableCount' FROM sqlite_master WHERE type='table' AND name='"+SCHEMA_TABLE_NAME+"';");
if (schemaTableExistsResult == null || (int) schemaTableExistsResult.get("tableCount") == 0)
{
boolean schemaTableMissing = schemaTableExistsResult.getInt(1) == 0;
if (schemaTableMissing)
{
// Note: if this table ever needs to be modified, that should be done via an auto update script to prevent issues with updating old databases
String createBaseSchemaTable =
"CREATE TABLE "+SCHEMA_TABLE_NAME+"( \n" +
" FileName TEXT NOT NULL PRIMARY KEY \n" +
" ,AppliedDateTime DATETIME NOT NULL default CURRENT_TIMESTAMP --in UTC 0 timezone \n" +
");";
repo.queryNoResult(createBaseSchemaTable);
}
// Note: if this table ever needs to be modified, that should be done via an auto update script to prevent issues with updating old databases
String createBaseSchemaTable =
"CREATE TABLE "+SCHEMA_TABLE_NAME+"( \n" +
" FileName TEXT NOT NULL PRIMARY KEY \n" +
" ,AppliedDateTime DATETIME NOT NULL default CURRENT_TIMESTAMP --in UTC 0 timezone \n" +
");";
repo.queryDictionaryFirst(createBaseSchemaTable);
}
// attempt to run any un-run update scripts
for (ResourceUtil.ResourceFile file : sqlScripts)
for (ResourceUtil.ResourceFile resource : sqlScripts)
{
ResultSet scriptAlreadyRunResult = repo.query("SELECT EXISTS(SELECT 1 FROM Schema WHERE FileName='"+file.name+"');");
if (!scriptAlreadyRunResult.next() || !scriptAlreadyRunResult.getBoolean(1))
Map<String, Object> scriptAlreadyRunResult = repo.queryDictionaryFirst("SELECT EXISTS(SELECT 1 FROM "+SCHEMA_TABLE_NAME+" WHERE FileName='"+resource.name+"') as 'existingCount';");
if (scriptAlreadyRunResult != null && (int) scriptAlreadyRunResult.get("existingCount") == 0)
{
LOGGER.info("Running SQL update script: ["+file.name+"], for repo: ["+repo.databaseLocation+"]");
repo.queryNoResult(file.content);
LOGGER.info("Running SQL update script: ["+resource.name+"], for repo: ["+repo.databaseLocation+"]");
try
{
repo.queryDictionaryFirst(resource.content);
}
catch (RuntimeException e)
{
// updating needs to stop to prevent any further data corruption
LOGGER.error("Unexpected error running database update script ["+resource.name+"] on database ["+repo.databaseLocation+"], stopping database update, data saving may fail. \n" +
"Error: ["+e.getMessage()+"]. \n" +
"Sql Script:["+resource.content+"]", e);
break;
}
// record the successfully run script
repo.queryNoResult("INSERT INTO Schema (FileName) VALUES('"+file.name+"');");
repo.queryDictionaryFirst("INSERT INTO "+SCHEMA_TABLE_NAME+" (FileName) VALUES('"+resource.name+"');");
}
}
}
@@ -19,14 +19,10 @@
package testItems.sql;
import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep;
import com.seibel.distanthorizons.core.file.fullDatafile.FullDataMetaFile;
import com.seibel.distanthorizons.core.file.metaData.BaseMetaData;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.sql.AbstractDhRepo;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
public class TestDataRepo extends AbstractDhRepo<TestDto>
{
@@ -38,12 +34,12 @@ public class TestDataRepo extends AbstractDhRepo<TestDto>
// note: this should only ever be done with the test repo.
// All long term tables should be created using a sql Script.
String createTableSql =
"CREATE TABLE "+this.getTableName()+"(\n" +
"CREATE TABLE IF NOT EXISTS "+this.getTableName()+"(\n" +
"Id INT NOT NULL PRIMARY KEY\n" +
"\n" +
",Value TEXT NULL\n" +
");";
this.queryNoResult(createTableSql);
this.queryDictionaryFirst(createTableSql);
}
@@ -55,18 +51,10 @@ public class TestDataRepo extends AbstractDhRepo<TestDto>
@Override
public TestDto convertResultSetToDto(ResultSet resultSet) throws SQLException
public TestDto convertDictionaryToDto(Map<String, Object> objectMap) throws ClassCastException
{
if (!resultSet.next())
{
return null;
}
String idString = resultSet.getString("Id");
int id = Integer.parseInt(idString);
String value = resultSet.getString("Value");
int id = (int) objectMap.get("Id");
String value = (String) objectMap.get("Value");
return new TestDto(id, value);
}
@@ -26,8 +26,8 @@ import testItems.sql.TestDataRepo;
import testItems.sql.TestDto;
import java.io.File;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
/**
* Validates {@link com.seibel.distanthorizons.core.sql.AbstractDhRepo} is set up correctly.
@@ -64,12 +64,17 @@ public class DhRepoSqliteTest
// Auto update script tests //
//==========================//
ResultSet autoUpdateTablePresentResult = testDataRepo.query("SELECT name FROM sqlite_master WHERE type='table' AND name='"+DatabaseUpdater.SCHEMA_TABLE_NAME+"';");
if (!autoUpdateTablePresentResult.next() || autoUpdateTablePresentResult.getString(1) == null)
// check that the schema table is created
Map<String, Object> autoUpdateTablePresentResult = testDataRepo.queryDictionaryFirst("SELECT name FROM sqlite_master WHERE type='table' AND name='"+DatabaseUpdater.SCHEMA_TABLE_NAME+"';");
if (autoUpdateTablePresentResult == null || autoUpdateTablePresentResult.get("name") == null)
{
Assert.fail("Auto DB update table missing.");
}
// check that the update scripts aren't run multiple times
TestDataRepo altDataRepoOne = new TestDataRepo(DATABASE_TYPE, dbFileName);
TestDataRepo altDataRepoTwo = new TestDataRepo(DATABASE_TYPE, dbFileName);
//===========//