Skip to content

Commit be7e700

Browse files
authored
Merge pull request #57 from SLM-Audio/syl/initial-db-values
Refactor to allow for initial values in DatabaseState
2 parents 4e6cc57 + 46ec221 commit be7e700

File tree

3 files changed

+130
-18
lines changed

3 files changed

+130
-18
lines changed

include/mostly_harmless/data/mostlyharmless_DatabaseState.h

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ namespace mostly_harmless::data {
2626
DoubleIndex = 5
2727
};
2828

29-
3029
template <DatabaseStorageType T>
3130
auto databaseQueryCallback(void* ud, int count, char** data, char** /*columns*/) -> int {
3231
auto* result = static_cast<std::optional<T>*>(ud);
@@ -48,6 +47,10 @@ namespace mostly_harmless::data {
4847
}
4948
} // namespace
5049

50+
/**
51+
* \brief A std::variant containing all types satisfying the DatabaseStorageType concept.
52+
*/
53+
using DatabaseValueVariant = std::variant<std::string, bool, int, float, double>;
5154
/**
5255
* \brief Represents a connection to a sqlite database.
5356
*
@@ -68,22 +71,68 @@ namespace mostly_harmless::data {
6871
/**
6972
* @private
7073
*/
71-
DatabaseState(Private, const std::filesystem::path& location) {
74+
DatabaseState(Private, const std::filesystem::path& location, const std::vector<std::pair<std::string, DatabaseValueVariant>>& initialValues) {
7275
const auto checkResult = [](int response) -> void {
7376
if (response != SQLITE_OK) {
7477
throw std::exception{};
7578
}
7679
};
77-
auto resultCode = sqlite3_open(location.string().c_str(), &m_databaseHandle);
78-
checkResult(resultCode);
79-
const std::string enableWalCommand{ "PRAGMA journal_mode=WAL" };
80-
resultCode = sqlite3_exec(m_databaseHandle, enableWalCommand.c_str(), nullptr, nullptr, nullptr);
81-
checkResult(resultCode);
82-
const std::string command{
83-
"CREATE TABLE IF NOT EXISTS DATA (NAME text UNIQUE, TEXT_VALUE text, BOOL_VALUE bool, INT_VALUE int, FLOAT_VALUE float, DOUBLE_VALUE double);"
84-
};
85-
resultCode = sqlite3_exec(m_databaseHandle, command.c_str(), nullptr, nullptr, nullptr);
86-
checkResult(resultCode);
80+
81+
// Try open existing
82+
if (sqlite3_open_v2(location.string().c_str(), &m_databaseHandle, SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK) {
83+
checkResult(sqlite3_open(location.string().c_str(), &m_databaseHandle)); // Didn't exist, try create
84+
}
85+
// Enable WAL
86+
checkResult(sqlite3_exec(m_databaseHandle, "PRAGMA journal_mode=WAL", nullptr, nullptr, nullptr));
87+
// Create Table if not present
88+
checkResult(sqlite3_exec(m_databaseHandle,
89+
"CREATE TABLE IF NOT EXISTS DATA (NAME text UNIQUE, TEXT_VALUE text, BOOL_VALUE bool, INT_VALUE int, FLOAT_VALUE float, DOUBLE_VALUE double);",
90+
nullptr,
91+
nullptr,
92+
nullptr));
93+
// Populate with initial values, if key is present already, skip set
94+
for (const auto& [key, value] : initialValues) {
95+
std::visit([this, &key](auto&& arg) {
96+
using T = std::decay_t<decltype(arg)>;
97+
if (get<T>(key)) {
98+
return;
99+
}
100+
set(key, std::forward<decltype(arg)>(arg));
101+
},
102+
value);
103+
}
104+
}
105+
106+
/**
107+
* Non Copyable, as the database connection pointer will be closed on destruction...
108+
*/
109+
DatabaseState(const DatabaseState& /*other*/) = delete;
110+
111+
/**
112+
* Moveable, nulls `other`'s connection pointer
113+
* @param other The moved-from DatabaseState instance
114+
*/
115+
DatabaseState(DatabaseState&& other) noexcept {
116+
std::swap(m_databaseHandle, other.m_databaseHandle);
117+
}
118+
119+
/**
120+
*
121+
* Non Copyable, as the database connection pointer will be closed on destruction...
122+
*
123+
*/
124+
DatabaseState& operator=(const DatabaseState& /*other*/) = delete;
125+
126+
/**
127+
* Moveable, nulls `other`'s connection pointer
128+
* @param other The moved-from DatabaseState instance
129+
* @return *this
130+
*/
131+
DatabaseState& operator=(DatabaseState&& other) noexcept {
132+
if (this != &other) {
133+
std::swap(m_databaseHandle, other.m_databaseHandle);
134+
}
135+
return *this;
87136
}
88137

89138
/**
@@ -92,20 +141,22 @@ namespace mostly_harmless::data {
92141
* If it doesn't exist, creates the database, and a table to store user data in.
93142
*
94143
* \param location A path to the database to create or open.
144+
* \param initialValues A vector containing the initial values to add to the database if it didn't exist. If the database DID exist, but any of the keys in the vector aren't present, they'll be added with the values specified
145+
* and existing items will be skipped.
95146
* \return A DatabaseState instance on success, nullopt otherwise.
96147
*/
97-
[[nodiscard]] static auto try_create(const std::filesystem::path& location) -> std::optional<DatabaseState> {
148+
[[nodiscard]] static auto try_create(const std::filesystem::path& location, const std::vector<std::pair<std::string, DatabaseValueVariant>>& initialValues) -> std::optional<DatabaseState> {
98149
try {
99-
DatabaseState state{ {}, location };
100-
return state;
150+
DatabaseState state{ {}, location, initialValues };
151+
return std::move(state);
101152
} catch (...) {
102153
assert(false);
103154
return {};
104155
}
105156
}
106157

107158
/**
108-
* @private
159+
* The internal database handle is closed if not null.
109160
*/
110161
~DatabaseState() noexcept {
111162
if (!m_databaseHandle) return;
@@ -119,8 +170,8 @@ namespace mostly_harmless::data {
119170
* @param toSet The value to set.
120171
*/
121172
template <DatabaseStorageType T>
122-
auto set(std::string_view name, T&& toSet) -> void {
123-
struct Properties {
173+
auto set(std::string_view name, const T& toSet) -> void {
174+
struct {
124175
std::string textValue{};
125176
bool boolValue{ false };
126177
int intValue{ 0 };

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ set(MOSTLYHARMLESS_TEST_SOURCE
44
${CMAKE_CURRENT_SOURCE_DIR}/utils/mostlyharmless_TaskThreadTests.cpp
55
${CMAKE_CURRENT_SOURCE_DIR}/utils/mostlyharmless_TimerTests.cpp
66
${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_InputEventContextTests.cpp
7+
${CMAKE_CURRENT_SOURCE_DIR}/data/mostlyharmless_DatabaseStateTests.cpp
78
PARENT_SCOPE)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//
2+
// Created by Syl Morrison on 12/04/2025.
3+
//
4+
#include <mostly_harmless/utils/mostlyharmless_Directories.h>
5+
#include <mostly_harmless/data/mostlyharmless_DatabaseState.h>
6+
#include <catch2/catch_test_macros.hpp>
7+
#include <catch2/matchers/catch_matchers_floating_point.hpp>
8+
9+
namespace mostly_harmless::testing {
10+
template <bool ShouldSucceed>
11+
auto tryCreateDatabase(const std::filesystem::path& destination, const std::vector<std::pair<std::string, data::DatabaseValueVariant>>& initialValues) {
12+
auto databaseOpt = data::DatabaseState::try_create(destination, initialValues);
13+
REQUIRE(databaseOpt.has_value() == ShouldSucceed);
14+
return std::move(databaseOpt);
15+
}
16+
17+
TEST_CASE("Test DatabaseState") {
18+
auto tempDir = utils::directories::getDirectory(utils::directories::DirectoryType::Temp);
19+
if (!tempDir) {
20+
REQUIRE(false);
21+
}
22+
auto dbFile = *tempDir / "moha_test_db.sqlite";
23+
SECTION("Test Valid Location, with no initial values") {
24+
{
25+
auto databaseOpt = tryCreateDatabase<true>(dbFile, {});
26+
auto& database = *databaseOpt;
27+
REQUIRE_NOTHROW(database.set<std::string>("Hello", "World"));
28+
const auto retrieved = database.get<std::string>("Hello");
29+
REQUIRE(retrieved.has_value());
30+
REQUIRE(*retrieved == "World");
31+
REQUIRE(!database.get<int>("aaaaa"));
32+
}
33+
{
34+
std::vector<std::pair<std::string, data::DatabaseValueVariant>> initialValues;
35+
initialValues.emplace_back("IntTest", 10);
36+
initialValues.emplace_back("DoubleTest", 15.0);
37+
auto databaseOpt = tryCreateDatabase<true>(dbFile, initialValues);
38+
auto& database = *databaseOpt;
39+
auto retrievedDouble = database.get<double>("DoubleTest");
40+
REQUIRE(retrievedDouble.has_value());
41+
REQUIRE_THAT(retrievedDouble.value(), Catch::Matchers::WithinRel(15.0));
42+
database.set<double>("DoubleTest", 20.0);
43+
retrievedDouble = database.get<double>("DoubleTest");
44+
REQUIRE(retrievedDouble.has_value());
45+
REQUIRE_THAT(retrievedDouble.value(), Catch::Matchers::WithinRel(20.0));
46+
auto database2Opt = tryCreateDatabase<true>(dbFile, initialValues);
47+
auto& database2 = *database2Opt;
48+
retrievedDouble = database2.get<double>("DoubleTest");
49+
REQUIRE(retrievedDouble.has_value());
50+
REQUIRE_THAT(retrievedDouble.value(), Catch::Matchers::WithinRel(20.0));
51+
}
52+
53+
std::filesystem::remove(dbFile);
54+
}
55+
SECTION("Test Invalid Location") {
56+
tryCreateDatabase<false>("/iamthelordofthebongo", {});
57+
}
58+
}
59+
60+
} // namespace mostly_harmless::testing

0 commit comments

Comments
 (0)