MostlyHarmless 0.0.1
 
Loading...
Searching...
No Matches
mostlyharmless_DatabaseState.h
Go to the documentation of this file.
1//
2// Created by Syl Morrison on 09/04/2025.
3//
4
5#ifndef MOSTLYHARMLESS_DATABASESTATE_H
6#define MOSTLYHARMLESS_DATABASESTATE_H
7
8#include <mostly_harmless/mostlyharmless_Concepts.h>
9#include <mostly_harmless/utils/mostlyharmless_Macros.h>
10#include <sqlite3.h>
11#include <fmt/core.h>
12#include <string>
13#include <filesystem>
14#include <variant>
15
17
21 namespace {
22 enum Index : size_t {
23 TextIndex = 1,
24 BoolIndex = 2,
25 IntIndex = 3,
26 FloatIndex = 4,
27 DoubleIndex = 5
28 };
29
30 template <DatabaseStorageType T>
31 auto databaseQueryCallback(void* ud, int count, char** data, char** /*columns*/) -> int {
32 auto* result = static_cast<std::optional<T>*>(ud);
33 if (count != 6) {
34 return SQLITE_FAIL;
35 }
36 if constexpr (std::same_as<T, std::string>) {
37 *result = data[Index::TextIndex];
38 } else if constexpr (std::same_as<T, bool>) {
39 *result = static_cast<T>(std::stoi(data[Index::BoolIndex]));
40 } else if constexpr (std::is_integral_v<T>) {
41 *result = static_cast<T>(std::stoi(data[Index::IntIndex]));
42 } else if constexpr (std::same_as<T, float>) {
43 *result = std::stof(data[Index::FloatIndex]);
44 } else if constexpr (std::same_as<T, double>) {
45 *result = std::stod(data[Index::DoubleIndex]);
46 }
47 return SQLITE_OK;
48 }
49 } // namespace
50
54 using DatabaseValueVariant = std::variant<std::string, bool, int, float, double>;
67 class DatabaseState final {
68 private:
69 struct Private {};
70
71 public:
75 DatabaseState(Private, const std::filesystem::path& location, const std::vector<std::pair<std::string, DatabaseValueVariant>>& initialValues) {
76 const auto checkResult = [](int response) -> void {
77 if (response != SQLITE_OK) {
78 throw std::exception{};
79 }
80 };
81
82 if (!std::filesystem::exists(location.parent_path()) && location.string() != ":memory:") {
83 throw std::exception{};
84 }
85 // Try open existing
86 if (sqlite3_open_v2(location.string().c_str(), &m_databaseHandle, SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK) {
87 checkResult(sqlite3_open(location.string().c_str(), &m_databaseHandle)); // Didn't exist, try create
88 }
89 // Enable WAL
90 checkResult(sqlite3_exec(m_databaseHandle, "PRAGMA journal_mode=WAL", nullptr, nullptr, nullptr));
91 // Create Table if not present
92 checkResult(sqlite3_exec(m_databaseHandle,
93 "CREATE TABLE IF NOT EXISTS DATA (NAME text UNIQUE, TEXT_VALUE text, BOOL_VALUE bool, INT_VALUE int, FLOAT_VALUE float, DOUBLE_VALUE double);",
94 nullptr,
95 nullptr,
96 nullptr));
97 // Populate with initial values, if key is present already, skip set
98 for (const auto& [key, value] : initialValues) {
99 std::visit([this, &key](auto&& arg) {
100 using T = std::decay_t<decltype(arg)>;
101 if (get<T>(key)) {
102 return;
103 }
104 set(key, std::forward<decltype(arg)>(arg));
105 },
106 value);
107 }
108 }
109
113 DatabaseState(const DatabaseState& /*other*/) = delete;
114
119 DatabaseState(DatabaseState&& other) noexcept {
120 std::swap(m_databaseHandle, other.m_databaseHandle);
121 }
122
128 DatabaseState& operator=(const DatabaseState& /*other*/) = delete;
129
135 DatabaseState& operator=(DatabaseState&& other) noexcept {
136 if (this != &other) {
137 std::swap(m_databaseHandle, other.m_databaseHandle);
138 }
139 return *this;
140 }
141
152 [[nodiscard]] static auto tryCreate(const std::filesystem::path& location, const std::vector<std::pair<std::string, DatabaseValueVariant>>& initialValues) -> std::optional<DatabaseState> {
153 try {
154 DatabaseState state{ {}, location, initialValues };
155 return std::move(state);
156 } catch (...) {
157 assert(false);
158 return {};
159 }
160 }
161
165 ~DatabaseState() noexcept {
166 if (!m_databaseHandle) return;
167 sqlite3_close(m_databaseHandle);
168 }
169
176 template <DatabaseStorageType T>
177 auto set(std::string_view name, const T& toSet) -> void {
178 struct {
179 std::string textValue{};
180 bool boolValue{ false };
181 int intValue{ 0 };
182 float floatValue{ 0.0f };
183 double doubleValue{ 0.0 };
184 } props;
185
186 std::string updateStr;
187 if constexpr (std::same_as<T, std::string>) {
188 props.textValue = toSet;
189 updateStr = fmt::format("TEXT_VALUE = \"{}\"", props.textValue);
190 } else if constexpr (std::same_as<T, bool>) {
191 props.boolValue = toSet;
192 updateStr = fmt::format("BOOL_VALUE = {}", props.boolValue);
193 } else if constexpr (std::is_integral_v<T>) {
194 props.intValue = static_cast<int>(toSet);
195 updateStr = fmt::format("INT_VALUE = {}", props.intValue);
196 } else if constexpr (std::same_as<T, float>) {
197 props.floatValue = toSet;
198 updateStr = fmt::format("FLOAT_VALUE = {}", props.floatValue);
199 } else if constexpr (std::same_as<T, double>) {
200 props.doubleValue = toSet;
201 updateStr = fmt::format("DOUBLE_VALUE = {}", props.doubleValue);
202 }
203
204 std::stringstream sstream;
205 sstream << "INSERT INTO DATA (NAME, TEXT_VALUE, BOOL_VALUE, INT_VALUE, FLOAT_VALUE, DOUBLE_VALUE)\n";
206 sstream << " VALUES ( \"" << name << "\", \"" << props.textValue << "\", " << props.boolValue << "," << props.intValue << ", " << props.floatValue << ", " << props.doubleValue << " )\n";
207 sstream << " ON CONFLICT(NAME) DO UPDATE SET " << updateStr << ";";
208 const auto execResult = sqlite3_exec(m_databaseHandle, sstream.str().c_str(), nullptr, nullptr, nullptr);
209 if (execResult != SQLITE_OK) {
210 MH_LOG(sqlite3_errmsg(m_databaseHandle));
211 assert(false);
212 }
213 }
214
221 template <DatabaseStorageType T>
222 [[nodiscard]] auto get(std::string_view name) -> std::optional<T> {
223 std::optional<T> result{};
224 const auto execStr = fmt::format("SELECT * FROM DATA WHERE NAME = \"{}\"", name);
225 const auto getRes = sqlite3_exec(m_databaseHandle,
226 execStr.c_str(),
227 databaseQueryCallback<T>,
228 static_cast<void*>(&result),
229 nullptr);
230 if (getRes != SQLITE_OK) {
231 MH_LOG(sqlite3_errmsg(m_databaseHandle));
232 assert(false);
233 }
234 return result;
235 }
236
245 [[nodiscard]] auto duplicate() const -> std::optional<DatabaseState> {
246 const auto filenameOpt = getDatabaseFilename();
247 if (!filenameOpt) {
248 return {};
249 }
250 const std::filesystem::path databasePath{ *filenameOpt };
251 if (!std::filesystem::exists(databasePath)) {
252 return {};
253 }
254 return tryCreate(databasePath, {});
255 }
256
257 private:
258 [[nodiscard]] auto getDatabaseFilename() const noexcept -> std::optional<std::string> {
259 const auto* res = sqlite3_db_filename(m_databaseHandle, "main");
260 if (!res || strcmp(res, "") == 0) {
261 return {};
262 }
263 return std::string{ res };
264 }
265 sqlite3* m_databaseHandle{ nullptr };
266 };
267
268
269} // namespace mostly_harmless::data
270
271#endif // MOSTLYHARMLESS_DATABASESTATE_H
static auto tryCreate(const std::filesystem::path &location, const std::vector< std::pair< std::string, DatabaseValueVariant > > &initialValues) -> std::optional< DatabaseState >
Definition mostlyharmless_DatabaseState.h:152
~DatabaseState() noexcept
Definition mostlyharmless_DatabaseState.h:165
DatabaseState & operator=(const DatabaseState &)=delete
DatabaseState & operator=(DatabaseState &&other) noexcept
Definition mostlyharmless_DatabaseState.h:135
DatabaseState(const DatabaseState &)=delete
DatabaseState(DatabaseState &&other) noexcept
Definition mostlyharmless_DatabaseState.h:119
auto set(std::string_view name, const T &toSet) -> void
Definition mostlyharmless_DatabaseState.h:177
auto duplicate() const -> std::optional< DatabaseState >
Definition mostlyharmless_DatabaseState.h:245
auto get(std::string_view name) -> std::optional< T >
Definition mostlyharmless_DatabaseState.h:222
Contains classes and functions related to data management.
Definition mostlyharmless_DatabaseState.h:16
std::variant< std::string, bool, int, float, double > DatabaseValueVariant
A std::variant containing all types satisfying the DatabaseStorageType concept.
Definition mostlyharmless_DatabaseState.h:54