Skip to content

Commit bf2aae9

Browse files
author
Igor Egorov
authored
Add SQLite for Persistent Storage (#64)
* Add SQLite wrapper * Add SQLite tests Signed-off-by: Igor Egorov <[email protected]>
1 parent 73211b6 commit bf2aae9

File tree

9 files changed

+319
-0
lines changed

9 files changed

+319
-0
lines changed

cmake/Hunter/config.cmake

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,9 @@ hunter_config(
3434
# disable building examples and tests, disable testing
3535
CMAKE_ARGS BOOST_DI_OPT_BUILD_TESTS=OFF BOOST_DI_OPT_BUILD_EXAMPLES=OFF
3636
)
37+
38+
hunter_config(
39+
SQLiteModernCpp
40+
URL https://github.com/soramitsu/libp2p-sqlite-modern-cpp/archive/fc3b700064cb57ab6b598c9bc7a12b2842f78da2.zip
41+
SHA1 d913f2a0360892a30bc7cd8820a0475800b47d76
42+
)

cmake/dependencies.cmake

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,7 @@ find_package(tsl_hat_trie CONFIG REQUIRED)
2929
# https://github.com/masterjedy/di
3030
hunter_add_package(Boost.DI)
3131
find_package(Boost.DI CONFIG REQUIRED)
32+
33+
# https://github.com/soramitsu/libp2p-sqlite-modern-cpp/tree/hunter
34+
hunter_add_package(SQLiteModernCpp)
35+
find_package(SQLiteModernCpp CONFIG REQUIRED)

include/libp2p/storage/sqlite.hpp

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/**
2+
* Copyright Soramitsu Co., Ltd. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
#ifndef LIBP2P_SQLITE_HPP
7+
#define LIBP2P_SQLITE_HPP
8+
9+
#include <vector>
10+
11+
#include <sqlite_modern_cpp.h>
12+
#include <libp2p/common/logger.hpp>
13+
14+
namespace libp2p::storage {
15+
16+
/// C++ handy interface for SQLite based on SQLiteModernCpp
17+
class SQLite {
18+
public:
19+
using StatementHandle = size_t;
20+
using database_binder = ::sqlite::database_binder;
21+
static auto constexpr kLoggerTag = "sqlite";
22+
23+
explicit SQLite(const std::string &db_file);
24+
SQLite(const std::string &db_file, const std::string &logger_tag);
25+
~SQLite();
26+
27+
template <typename T>
28+
auto operator<<(const T &t) {
29+
return (db_ << t);
30+
}
31+
32+
template <typename T>
33+
SQLite &operator>>(T &t) {
34+
db_ >> t;
35+
return *this;
36+
}
37+
38+
/// Reads extended sqlite3 error code
39+
int getErrorCode();
40+
41+
/// Returns human-readable representation of an error
42+
std::string getErrorMessage();
43+
44+
/**
45+
* Store prepared statement
46+
* @param sql - prepared statement body
47+
* @return - handle to the statement for
48+
*/
49+
StatementHandle createStatement(const std::string &sql);
50+
51+
/**
52+
* Retrieves SQLiteModernCpp prepared statement
53+
* @param handle - statement identifier
54+
* @return - statement bound to the db
55+
*/
56+
database_binder &getStatement(StatementHandle handle);
57+
58+
/**
59+
* Executes a command from a prepared statement
60+
* @tparam Args - command arguments' types
61+
* @param st_handle - statement identifier
62+
* @param args - command arguments
63+
* @return number of rows affected, -1 in case of error
64+
*/
65+
template <typename... Args>
66+
inline int execCommand(StatementHandle st_handle,
67+
const Args &... args) noexcept {
68+
try {
69+
auto &st = getStatement(st_handle);
70+
bindArgs(st, args...);
71+
st.execute();
72+
return countChanges();
73+
} catch (const std::runtime_error &e) {
74+
// getStatement can receive invalid handle
75+
log_->error(e.what());
76+
} catch (...) {
77+
log_->error(getErrorMessage());
78+
}
79+
return -1;
80+
}
81+
82+
/**
83+
* Executes a query from a prepared statement
84+
* @tparam Sink - query response consumer type
85+
* @tparam Args - query arguments' types
86+
* @param st_handle - statement identifier
87+
* @param sink - query response consumer
88+
* @param args - query arguments
89+
* @return true when query was successfully executed, otherwise - false
90+
*/
91+
template <typename Sink, typename... Args>
92+
inline bool execQuery(StatementHandle st_handle, Sink &&sink,
93+
const Args &... args) noexcept {
94+
try {
95+
auto &st = getStatement(st_handle);
96+
bindArgs(st, args...);
97+
st >> sink;
98+
return true;
99+
} catch (const std::runtime_error &e) {
100+
// getStatement can receive invalid handle
101+
log_->error(e.what());
102+
} catch (...) {
103+
log_->error(getErrorMessage());
104+
}
105+
return false;
106+
}
107+
108+
private:
109+
static inline database_binder &bindArgs(database_binder &statement) {
110+
return statement;
111+
}
112+
113+
template <typename Arg, typename... Args>
114+
static inline database_binder &bindArgs(database_binder &statement,
115+
const Arg &arg,
116+
const Args &... args) {
117+
statement << arg;
118+
return bindArgs(statement, args...);
119+
}
120+
121+
/// Returns the number of rows modified
122+
int countChanges();
123+
124+
::sqlite::database db_;
125+
std::string db_file_;
126+
common::Logger log_;
127+
128+
std::vector<database_binder> statements_;
129+
};
130+
131+
} // namespace libp2p::storage
132+
133+
#endif // LIBP2P_SQLITE_HPP

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ add_subdirectory(peer)
1414
add_subdirectory(protocol)
1515
add_subdirectory(protocol_muxer)
1616
add_subdirectory(security)
17+
add_subdirectory(storage)
1718
add_subdirectory(transport)
1819

1920
add_library(p2p

src/storage/CMakeLists.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#
2+
# Copyright Soramitsu Co., Ltd. All Rights Reserved.
3+
# SPDX-License-Identifier: Apache-2.0
4+
#
5+
6+
libp2p_add_library(libp2p_sqlite sqlite.cpp)
7+
target_link_libraries(libp2p_sqlite
8+
SQLiteModernCpp::SQLiteModernCpp
9+
p2p_logger
10+
)

src/storage/sqlite.cpp

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Copyright Soramitsu Co., Ltd. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
#include <libp2p/storage/sqlite.hpp>
7+
8+
namespace libp2p::storage {
9+
10+
SQLite::SQLite(const std::string &db_file)
11+
: db_(db_file),
12+
db_file_(db_file),
13+
log_(common::createLogger(kLoggerTag)) {}
14+
15+
SQLite::SQLite(const std::string &db_file, const std::string &logger_tag)
16+
: db_(db_file),
17+
db_file_(db_file),
18+
log_(common::createLogger(logger_tag)) {}
19+
20+
SQLite::~SQLite() {
21+
// without the following, all the prepared statements
22+
// might be executed when db_'s destructor is called
23+
for (auto &st : statements_) {
24+
st.used(true);
25+
}
26+
}
27+
28+
int SQLite::getErrorCode() {
29+
return sqlite3_extended_errcode(db_.connection().get());
30+
}
31+
32+
std::string SQLite::getErrorMessage() {
33+
int ec{getErrorCode()};
34+
return (0 == ec) ? std::string()
35+
: std::string(sqlite3_errstr(ec)) + ": "
36+
+ sqlite3_errmsg(db_.connection().get());
37+
}
38+
39+
SQLite::StatementHandle SQLite::createStatement(const std::string &sql) {
40+
auto handle{statements_.size()};
41+
statements_.emplace_back(db_ << sql);
42+
return handle;
43+
}
44+
45+
SQLite::database_binder &SQLite::getStatement(
46+
SQLite::StatementHandle handle) {
47+
if (handle >= statements_.size()) {
48+
throw std::runtime_error("SQLite: statement does not exist");
49+
}
50+
return statements_[handle];
51+
}
52+
53+
int SQLite::countChanges() {
54+
return sqlite3_changes(db_.connection().get());
55+
}
56+
} // namespace libp2p::storage

test/libp2p/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ add_subdirectory(peer)
1616
add_subdirectory(protocol)
1717
add_subdirectory(protocol_muxer)
1818
add_subdirectory(security)
19+
add_subdirectory(storage)
1920
add_subdirectory(transport)

test/libp2p/storage/CMakeLists.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#
2+
# Copyright Soramitsu Co., Ltd. All Rights Reserved.
3+
# SPDX-License-Identifier: Apache-2.0
4+
#
5+
6+
addtest(libp2p_sqlite_test
7+
sqlite_test.cpp
8+
)
9+
target_link_libraries(libp2p_sqlite_test
10+
Boost::filesystem
11+
libp2p_sqlite
12+
)

test/libp2p/storage/sqlite_test.cpp

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* Copyright Soramitsu Co., Ltd. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
#include <libp2p/storage/sqlite.hpp>
7+
8+
#include <vector>
9+
10+
#include <gtest/gtest.h>
11+
#include <boost/filesystem.hpp>
12+
13+
/// Fixture for in-memory SQLite tests
14+
struct SQLite : public ::testing::Test {
15+
SQLite() : sql(":memory:") {}
16+
libp2p::storage::SQLite sql;
17+
};
18+
19+
/// Operators << and >> provide raw access to SQLite
20+
TEST_F(SQLite, RawOperators) {
21+
int result = 0;
22+
sql << "SELECT 1+1" >> result;
23+
ASSERT_EQ(result, 2);
24+
}
25+
26+
/**
27+
* Preapared statement can be used more than once
28+
* @given a database with a table
29+
* @when the table is filled in with multiple calls of a prepared statement
30+
* @then table contents can be queries with another prepared statement more than
31+
* once
32+
*/
33+
TEST_F(SQLite, MultipleUseOfPreparedStatement) {
34+
sql << "create table countable(num integer, char text);";
35+
auto handle =
36+
sql.createStatement("insert into countable(num, char) values(?, ?)");
37+
const char chars[] = {"abcdef"};
38+
for (size_t i = 0; i < sizeof(chars); ++i) {
39+
sql.execCommand(handle, i, chars[i]);
40+
}
41+
42+
auto query = sql.createStatement("select sum(num) from countable");
43+
auto checker = [](int sum) {
44+
ASSERT_EQ(sum, 21); // 21 is sum of numbers from 1 to 6
45+
};
46+
sql.execQuery(query, checker);
47+
sql.execQuery(query, checker);
48+
}
49+
50+
/// Fixture for tests which require a file being saved on disk
51+
struct Persistence : public ::testing::Test {
52+
Persistence() : kTestDbFile("test_db.sqlite") {}
53+
54+
~Persistence() {
55+
// this code will be executed even if test has thrown an exception
56+
if (boost::filesystem::exists(kTestDbFile)) {
57+
boost::filesystem::remove(kTestDbFile);
58+
}
59+
}
60+
61+
std::string char2string(const char c) {
62+
return std::string(1, c);
63+
}
64+
65+
const std::string kTestDbFile;
66+
};
67+
68+
/**
69+
* Database saved on disk preserves stored state
70+
* @given an SQLite wrapper which saves the db to kTestDbFile
71+
* @when the db is filled with the data and gets closed
72+
* @then the db file can be opened and the data is still available
73+
*/
74+
TEST_F(Persistence, StatePreserved) {
75+
const char chars[] = "abcdef";
76+
{
77+
libp2p::storage::SQLite sql1(kTestDbFile);
78+
sql1 << "create table countable(num integer, char text);";
79+
auto handle =
80+
sql1.createStatement("insert into countable(num, char) values(?, ?)");
81+
82+
// loop up to sizeof -1 because of null terminator of C-string
83+
for (size_t i = 0; i < sizeof(chars) - 1; ++i) {
84+
sql1.execCommand(handle, i, char2string(chars[i]));
85+
}
86+
}
87+
{
88+
libp2p::storage::SQLite sql2(kTestDbFile);
89+
auto checker = [this, chars](int number, std::string c) {
90+
ASSERT_GE(number, 0);
91+
ASSERT_LT(number, sizeof(chars));
92+
ASSERT_EQ(c, char2string(chars[number]));
93+
};
94+
sql2 << "select num, char from countable" >> checker;
95+
}
96+
}

0 commit comments

Comments
 (0)