From ebef91601e961e7f0020aa7524d4d7108346ef72 Mon Sep 17 00:00:00 2001 From: James Seibel Date: Sat, 30 Sep 2023 15:25:35 -0500 Subject: [PATCH] add basic sqlite and unit tests --- .../core/sql/AbstractDhRepo.java | 211 ++++++++++++++++++ .../distanthorizons/core/sql/IBaseDTO.java | 26 +++ .../test/java/testItems/sql/TestDataRepo.java | 97 ++++++++ core/src/test/java/testItems/sql/TestDto.java | 38 ++++ .../src/test/java/tests/DhRepoSqliteTest.java | 106 +++++++++ core/src/test/java/tests/SqliteSetupTest.java | 121 ++++++++++ 6 files changed, 599 insertions(+) create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/sql/AbstractDhRepo.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/sql/IBaseDTO.java create mode 100644 core/src/test/java/testItems/sql/TestDataRepo.java create mode 100644 core/src/test/java/testItems/sql/TestDto.java create mode 100644 core/src/test/java/tests/DhRepoSqliteTest.java create mode 100644 core/src/test/java/tests/SqliteSetupTest.java diff --git a/core/src/main/java/com/seibel/distanthorizons/core/sql/AbstractDhRepo.java b/core/src/main/java/com/seibel/distanthorizons/core/sql/AbstractDhRepo.java new file mode 100644 index 000000000..93af0230c --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/sql/AbstractDhRepo.java @@ -0,0 +1,211 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.core.sql; + +import org.jetbrains.annotations.Nullable; +import org.junit.Assert; + +import java.io.*; +import java.net.URL; +import java.sql.*; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Scanner; +import java.util.stream.Collectors; + +/** + * + * @param DTO stands for "Data Table Object" + */ +public abstract class AbstractDhRepo +{ + public static final int TIMEOUT_SECONDS = 30; + + private final Connection connection; + + public final String databaseType; + public final String databaseLocation; + + public final Class dtoClass; + + + + // constructor // + + public AbstractDhRepo(String databaseType, String databaseLocation, Class dtoClass) throws SQLException + { + this.databaseType = databaseType; + this.databaseLocation = databaseLocation; + this.dtoClass = dtoClass; + + this.connection = DriverManager.getConnection(this.databaseType+":"+this.databaseLocation); + + this.runFirstTimeSetup(); + } + private void runFirstTimeSetup() + { + // get all sql scripts + ArrayList sqlScriptNames = new ArrayList<>(); + try + { + Enumeration resources = Thread.currentThread().getContextClassLoader().getResources("sqlScripts"); // package/name/with/slashes/instead/dots + while (resources.hasMoreElements()) + { + URL url = resources.nextElement(); + + String fileName = new Scanner((InputStream) url.getContent()).useDelimiter("\\A").next(); + fileName = fileName.trim(); + + if (fileName.endsWith(".sql")) + { + sqlScriptNames.add(fileName); + } + } + } + catch (IOException e) + { + + } + + // attempt to run them + for (String scriptName : sqlScriptNames) + { + InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("sqlScripts/"+scriptName); + if (inputStream != null) + { + String script = new BufferedReader(new InputStreamReader(inputStream)).lines().collect(Collectors.joining("\n")); + System.out.println("Running script: "+scriptName); + + this.queryNoResult(script); + } + } + } + + + + // high level DB // + + public TDTO getByPrimaryKey(String primaryKey) + { + TDTO dto = null; + try + { + ResultSet resultSet = this.query(this.createSelectPrimaryKeySql(primaryKey)); + dto = this.convertResultSetToDto(resultSet); + resultSet.close(); + } + catch (SQLException e) + { + System.err.println(e.getMessage()); + } + + return dto; + } + + public void save(TDTO dto) + { + if (this.getByPrimaryKey(dto.getPrimaryKeyString()) != null) + { + this.update(dto); + } + else + { + this.insert(dto); + } + } + private void insert(TDTO dto) { this.queryNoResult(this.createInsertSql(dto)); } + private void update(TDTO dto) { this.queryNoResult(this.createUpdateSql(dto)); } + + + + // low level DB // + + public void queryNoResult(String sql) { this.query(sql, false); } + public ResultSet query(String sql) { return this.query(sql, true); } + + @Nullable + private ResultSet query(String sql, boolean returnResultSet) + { + try + { + Statement statement = this.connection .createStatement(); + statement.setQueryTimeout(TIMEOUT_SECONDS); + + boolean resultSetPresent = statement.execute(sql); + if (resultSetPresent) + { + ResultSet resultSet = statement.getResultSet(); + if (returnResultSet) + { + return resultSet; + } + else + { + resultSet.close(); + return null; + } + } + else + { + return null; + } + } + catch(SQLException e) + { + // if the error message is "out of memory", + // it probably means no database file is found + Assert.fail("Unexpected error for query ["+sql+"]: " + e.getMessage()); + } + + return null; + } + + + public void close() + { + try + { + if(this.connection != null) + { + this.connection.close(); + } + } + catch(SQLException e) + { + // connection close failed. + Assert.fail("Unable to close the connection: " + e.getMessage()); + } + } + + + + // abstract // + + public abstract String getTableName(); + + @Nullable + public abstract TDTO convertResultSetToDto(ResultSet resultSet) throws SQLException; + + public abstract String createSelectPrimaryKeySql(String primaryKey); + + public abstract String createInsertSql(TDTO dto); + public abstract String createUpdateSql(TDTO dto); + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/sql/IBaseDTO.java b/core/src/main/java/com/seibel/distanthorizons/core/sql/IBaseDTO.java new file mode 100644 index 000000000..634e3fd2c --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/sql/IBaseDTO.java @@ -0,0 +1,26 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.core.sql; + +public interface IBaseDTO +{ + String getPrimaryKeyString(); + +} diff --git a/core/src/test/java/testItems/sql/TestDataRepo.java b/core/src/test/java/testItems/sql/TestDataRepo.java new file mode 100644 index 000000000..b07433966 --- /dev/null +++ b/core/src/test/java/testItems/sql/TestDataRepo.java @@ -0,0 +1,97 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +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; + +public class TestDataRepo extends AbstractDhRepo +{ + + public TestDataRepo(String databaseType, String databaseLocation) throws SQLException + { + super(databaseType, databaseLocation, TestDto.class); + + // 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" + + "Id INT NOT NULL PRIMARY KEY\n" + + "\n" + + ",Value TEXT NULL\n" + + ");"; + this.queryNoResult(createTableSql); + } + + + + @Override + public String getTableName() { return "Test"; } + + @Override + public TestDto convertResultSetToDto(ResultSet resultSet) throws SQLException + { + if (!resultSet.next()) + { + return null; + } + + + String idString = resultSet.getString("Id"); + int id = Integer.parseInt(idString); + + String value = resultSet.getString("Value"); + + return new TestDto(id, value); + } + + @Override + public String createSelectPrimaryKeySql(String primaryKey) { return "SELECT * FROM "+this.getTableName()+" WHERE Id = '"+primaryKey+"'"; } + + @Override + public String createInsertSql(TestDto dto) + { + String id = dto.id+""; + String value = (dto.value != null) ? dto.value+"" : "NULL"; + + return + "INSERT INTO "+this.getTableName()+" (Id, Value) " + + "VALUES("+id+",'"+value+"');"; + } + + @Override + public String createUpdateSql(TestDto dto) + { + String id = dto.id+""; + String value = (dto.value != null) ? dto.value+"" : "NULL"; + + return + "UPDATE "+this.getTableName()+" " + + "SET Value = '"+value+"' " + + "WHERE Id = "+id; + } + +} diff --git a/core/src/test/java/testItems/sql/TestDto.java b/core/src/test/java/testItems/sql/TestDto.java new file mode 100644 index 000000000..b3660fa07 --- /dev/null +++ b/core/src/test/java/testItems/sql/TestDto.java @@ -0,0 +1,38 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package testItems.sql; + +import com.seibel.distanthorizons.core.sql.IBaseDTO; + +public class TestDto implements IBaseDTO +{ + public int id; + public String value; + + public TestDto(int id, String value) + { + this.id = id; + this.value = value; + } + + @Override + public String getPrimaryKeyString() { return this.id+""; } + +} diff --git a/core/src/test/java/tests/DhRepoSqliteTest.java b/core/src/test/java/tests/DhRepoSqliteTest.java new file mode 100644 index 000000000..e781006c0 --- /dev/null +++ b/core/src/test/java/tests/DhRepoSqliteTest.java @@ -0,0 +1,106 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package tests; + +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.FullMetaDataRepo; +import org.junit.Assert; +import org.junit.Test; +import testItems.sql.TestDataRepo; +import testItems.sql.TestDto; + +import java.io.File; +import java.sql.SQLException; + +/** + * Validates {@link com.seibel.distanthorizons.core.sql.AbstractDhRepo} is set up correctly. + */ +public class DhRepoSqliteTest +{ + public static String DATABASE_TYPE = "jdbc:sqlite"; + + + @Test + public void testFileSqlite() + { + String dbFileName = "test_fullData.sqlite"; + + + File dbFile = new File(dbFileName); + if (dbFile.exists()) + { + Assert.assertTrue("unable to delete old test DB File.", dbFile.delete()); + } + + + TestDataRepo testDataRepo = null; + try + { + testDataRepo = new TestDataRepo(DATABASE_TYPE, dbFileName); + + dbFile = new File(dbFileName); + Assert.assertTrue("dbFile not created", dbFile.exists()); + + + // insert + TestDto insertDto = new TestDto(0, "a"); + testDataRepo.save(insertDto); + + // get + TestDto getDto = testDataRepo.getByPrimaryKey("0"); + Assert.assertNotNull("get failed, null returned", getDto); + Assert.assertEquals("get/insert failed, not equal", insertDto.id, getDto.id); + Assert.assertEquals("get/insert failed, not equal", insertDto.value, getDto.value); + + + // update + TestDto updateMetaFile = new TestDto(0, "b"); + testDataRepo.save(updateMetaFile); + + // get + getDto = testDataRepo.getByPrimaryKey("0"); + Assert.assertNotNull("get failed, null returned", getDto); + Assert.assertEquals("get/insert failed, not equal", updateMetaFile.id, getDto.id); + Assert.assertEquals("get/insert failed, not equal", updateMetaFile.value, getDto.value); + + } + catch (SQLException e) + { + Assert.fail(e.getMessage()); + } + finally + { + if (testDataRepo != null) + { + testDataRepo.close(); + } + + dbFile = new File(dbFileName); + if (dbFile.exists()) + { + Assert.assertTrue("unable to delete test DB File.", dbFile.delete()); + } + } + } + +} diff --git a/core/src/test/java/tests/SqliteSetupTest.java b/core/src/test/java/tests/SqliteSetupTest.java new file mode 100644 index 000000000..350e8edbd --- /dev/null +++ b/core/src/test/java/tests/SqliteSetupTest.java @@ -0,0 +1,121 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package tests; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.File; +import java.sql.*; + +/** + * Validates Sqlite is setup correctly. + */ +public class SqliteSetupTest +{ + public static String DATABASE_TYPE = "jdbc:sqlite"; + + + @Test + public void testSqliteFile() + { + String databaseLocation = "sample.sqlite"; + testSqliteDatabase(DATABASE_TYPE, databaseLocation); + + File dbFile = new File(databaseLocation); + Assert.assertTrue("Unable to delete test database.", dbFile.delete()); + } + + @Test + public void testInMemorySqlite() + { + String databaseLocation = ":memory:"; + testSqliteDatabase(DATABASE_TYPE, databaseLocation); + } + + + + //================// + // helper methods // + //================// + + private static void testSqliteDatabase(String databaseType, String databaseLocation) + { + Connection connection = null; + try + { + // create a database connection + connection = DriverManager.getConnection(databaseType+":"+databaseLocation); + Statement statement = connection.createStatement(); + statement.setQueryTimeout(30); // set timeout to 30 sec. + + + // create the database + statement.executeUpdate("drop table if exists person"); + statement.executeUpdate("create table person (id integer, name string)"); + + // insert test values + statement.executeUpdate("insert into person values(1, 'leo')"); + statement.executeUpdate("insert into person values(2, 'yui')"); + + // get the values + ResultSet rs = statement.executeQuery("select * from person"); + + + // read the result set + Assert.assertTrue(rs.next()); + int id = rs.getInt("id"); + Assert.assertEquals(1, id); + String name = rs.getString("name"); + Assert.assertEquals("leo", name); + + Assert.assertTrue(rs.next()); + id = rs.getInt("id"); + Assert.assertEquals(2, id); + name = rs.getString("name"); + Assert.assertEquals("yui", name); + + // all results have been read + Assert.assertFalse(rs.next()); + } + catch(SQLException e) + { + // if the error message is "out of memory", + // it probably means no database file is found + Assert.fail("Unexpected error: " + e.getMessage()); + } + finally + { + try + { + if(connection != null) + { + connection.close(); + } + } + catch(SQLException e) + { + // connection close failed. + Assert.fail("Unable to close the connection: " + e.getMessage()); + } + } + } + +}