Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ SQLite's serialized mode and all operations are serialized internally, so concur
`Save`/`Fetch`/`Update`/`Delete` calls are safe (they execute one at a time rather than
in parallel). `Initialize`, `Finalize`, and `Instance` are also safe to call concurrently.

**Ownership model.** `Instance()` returns a `std::shared_ptr<const Database>` rather than a
**Ownership model.** `Instance()` returns a `std::shared_ptr<Database>` rather than a
reference. Each user holds a strong handle, so the database stays alive for as long as anyone
is using it: an operation in progress on one thread is never torn down by a concurrent
`Finalize()` on another. `Finalize()` only drops the singleton's own handle; the connection is
Expand All @@ -197,6 +197,17 @@ std::thread worker([db] {
worker.join();
```

The write methods (`Save`, `SaveAutoIncrement`, `Update`, `Delete`, `UnsafeSql`) are
non-`const`, and the fetch methods are `const`. To hand a component a read-only view, assign
the handle to a `std::shared_ptr<const Database>` (the conversion is implicit) — through it,
only the fetch methods compile:

```c++
std::shared_ptr<const Database> reader = Database::Instance();
auto people = reader->FetchAll<Person>(); // ok
// reader->Save(person); // does not compile: Save is not const
```

Writes (and reads) are serialized, so they are safe but not concurrent; true write parallelism
is tracked as future work.

Expand Down
34 changes: 18 additions & 16 deletions include/database.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,10 @@ class REFLECTION_EXPORT Database {

/// Retrieves the database singleton wrapper for further operations. The returned shared
/// handle keeps the database alive for as long as the caller holds it, so an operation in
/// progress is never torn down by a concurrent Finalize().
static std::shared_ptr<const Database> Instance();
/// progress is never torn down by a concurrent Finalize(). The handle is writable; for a
/// read-only view, assign it to a std::shared_ptr<const Database> (the conversion is
/// implicit), through which only the const fetch methods are callable.
static std::shared_ptr<Database> Instance();

~Database();

Expand Down Expand Up @@ -99,7 +101,7 @@ class REFLECTION_EXPORT Database {
/// Saves a given record in the database.
/// This corresponds to an INSERT query in the SQL syntax
template <typename T>
void Save(const T& model) const {
void Save(const T& model) {
const auto type_id = typeid(T).name();
const auto& record = GetRecord(type_id);
Save((void*)&model, record);
Expand All @@ -109,7 +111,7 @@ class REFLECTION_EXPORT Database {
/// The newly generated id is written back into the passed-in model.
/// This corresponds to an INSERT query in the SQL syntax
template <typename T>
void SaveAutoIncrement(T& model) const {
void SaveAutoIncrement(T& model) {
const auto type_id = typeid(T).name();
const auto& record = GetRecord(type_id);
model.id = SaveAutoIncrement((void*)&model, record);
Expand All @@ -118,7 +120,7 @@ class REFLECTION_EXPORT Database {
/// Saves multiple records iteratively in the database.
/// This corresponds to an INSERT query in the SQL syntax
template <typename T>
void Save(const std::vector<T>& models) const {
void Save(const std::vector<T>& models) {
const auto type_id = typeid(T).name();
const auto& record = GetRecord(type_id);
for (const auto& model : models) {
Expand All @@ -130,7 +132,7 @@ class REFLECTION_EXPORT Database {
/// The newly generated ids are written back into the passed-in models.
/// This corresponds to an INSERT query in the SQL syntax
template <typename T>
void SaveAutoIncrement(std::vector<T>& models) const {
void SaveAutoIncrement(std::vector<T>& models) {
const auto type_id = typeid(T).name();
const auto& record = GetRecord(type_id);
for (auto& model : models) {
Expand All @@ -141,7 +143,7 @@ class REFLECTION_EXPORT Database {
/// Updates a given record in the database.
/// This corresponds to an UPDATE query in the SQL syntax
template <typename T>
void Update(const T& model) const {
void Update(const T& model) {
const auto type_id = typeid(T).name();
const auto& record = GetRecord(type_id);
Update((void*)&model, record);
Expand All @@ -150,7 +152,7 @@ class REFLECTION_EXPORT Database {
/// Updates multiple records iteratively in the database.
/// This corresponds to an UPDATE query in the SQL syntax
template <typename T>
void Update(const std::vector<T>& models) const {
void Update(const std::vector<T>& models) {
const auto type_id = typeid(T).name();
const auto& record = GetRecord(type_id);
for (const auto& model : models) {
Expand All @@ -161,7 +163,7 @@ class REFLECTION_EXPORT Database {
/// Deletes a given record from the database.
/// This corresponds to an DELETE query in the SQL syntax
template <typename T>
void Delete(const T& model) const {
void Delete(const T& model) {
const auto type_id = typeid(T).name();
const auto& record = GetRecord(type_id);
const auto equal_id_predicate = Equal(&T::id, model.id);
Expand All @@ -171,7 +173,7 @@ class REFLECTION_EXPORT Database {
/// Deletes a given record from the database, which matches a given id.
/// This corresponds to an DELETE query in the SQL syntax
template <typename T>
void Delete(int64_t id) const {
void Delete(int64_t id) {
const auto type_id = typeid(T).name();
const auto& record = GetRecord(type_id);
const auto equal_id_predicate = Equal(&T::id, id);
Expand All @@ -181,15 +183,15 @@ class REFLECTION_EXPORT Database {
/// Deletes multiple records of a given type from the database, which match a given predicate.
/// This corresponds to an DELETE query in the SQL syntax, with an additional WHERE clause
template <typename T>
void Delete(const QueryPredicateBase* predicate) const {
void Delete(const QueryPredicateBase* predicate) {
const auto type_id = typeid(T).name();
const auto& record = GetRecord(type_id);
Delete(record, predicate);
}

/// Executes a raw SQL query. A trailing semicolon is added if needed.
/// Prefer the type-safe CRUD APIs for user-provided values.
void UnsafeSql(const std::string& raw_sql_query) const;
void UnsafeSql(const std::string& raw_sql_query);

private:
explicit Database(const char* path);
Expand All @@ -214,17 +216,17 @@ class REFLECTION_EXPORT Database {
}

/// Saves a single record in the database
void Save(void* p, const Reflection& record) const;
void Save(void* p, const Reflection& record);

/// Saves a single record in the database, letting SQLite assign its id,
/// and returns the id that was generated for the inserted row
int64_t SaveAutoIncrement(void* p, const Reflection& record) const;
int64_t SaveAutoIncrement(void* p, const Reflection& record);

/// Updates a single record in the database
void Update(void* p, const Reflection& record) const;
void Update(void* p, const Reflection& record);

/// Deletes a single record from the database
void Delete(const Reflection& record, const QueryPredicateBase* predicate) const;
void Delete(const Reflection& record, const QueryPredicateBase* predicate);

static std::shared_ptr<Database> instance_;

Expand Down
12 changes: 6 additions & 6 deletions src/database.cc
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Database::Database(const char* path) : db_(nullptr) {
}
}

std::shared_ptr<const Database> Database::Instance() {
std::shared_ptr<Database> Database::Instance() {
std::lock_guard<std::mutex> lock(instance_mutex_);
if (instance_ == nullptr) {
throw std::runtime_error("Database has not been initialized; call Database::Initialize() first");
Expand All @@ -117,13 +117,13 @@ const Reflection& Database::GetRecord(const std::string& type_id) {
return GetReflectionRegister().records.at(type_id);
}

void Database::Save(void* p, const Reflection& record) const {
void Database::Save(void* p, const Reflection& record) {
std::lock_guard<std::mutex> lock(db_mutex_);
InsertQuery query(db_, record, p);
query.Execute();
}

int64_t Database::SaveAutoIncrement(void* p, const Reflection& record) const {
int64_t Database::SaveAutoIncrement(void* p, const Reflection& record) {
// The lock spans both the insert and the rowid read so that, on the shared connection,
// sqlite3_last_insert_rowid() reflects this insert and not one from another thread.
std::lock_guard<std::mutex> lock(db_mutex_);
Expand All @@ -132,19 +132,19 @@ int64_t Database::SaveAutoIncrement(void* p, const Reflection& record) const {
return sqlite3_last_insert_rowid(db_);
}

void Database::Update(void* p, const Reflection& record) const {
void Database::Update(void* p, const Reflection& record) {
std::lock_guard<std::mutex> lock(db_mutex_);
UpdateQuery query(db_, record, p);
query.Execute();
}

void Database::Delete(const Reflection& record, const QueryPredicateBase* predicate) const {
void Database::Delete(const Reflection& record, const QueryPredicateBase* predicate) {
std::lock_guard<std::mutex> lock(db_mutex_);
DeleteQuery query(db_, record, predicate);
query.Execute();
}

void Database::UnsafeSql(const std::string& raw_sql_query) const {
void Database::UnsafeSql(const std::string& raw_sql_query) {
std::lock_guard<std::mutex> lock(db_mutex_);
SqlQuery sql(db_, raw_sql_query);
sql.Execute();
Expand Down
49 changes: 49 additions & 0 deletions tests/database_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,49 @@

#include <gtest/gtest.h>

#include <memory>
#include <type_traits>
#include <utility>

#include "company.h"
#include "id_only_record.h"
#include "person.h"
#include "pet.h"

using namespace sqlite_reflection;

namespace {
// C++11 stand-in for std::void_t (C++17)
template <typename...>
struct MakeVoid {
typedef void type;
};

// Detects whether Save(const T&) is invocable through a const Database
template <typename T, typename = void>
struct CanSaveThroughConst : std::false_type {};

template <typename T>
struct CanSaveThroughConst<
T, typename MakeVoid<decltype(std::declval<const Database&>().Save(std::declval<const T&>()))>::type>
: std::true_type {};

// Detects whether FetchAll<T>() is invocable through a const Database
template <typename T, typename = void>
struct CanFetchAllThroughConst : std::false_type {};

template <typename T>
struct CanFetchAllThroughConst<T, typename MakeVoid<decltype(std::declval<const Database&>().FetchAll<T>())>::type>
: std::true_type {};
} // namespace

// The const-correctness contract (#26): const means read-only. A handle to a const Database
// can fetch but not mutate; the write methods are only invocable through a non-const handle.
static_assert(!CanSaveThroughConst<Person>::value,
"Save must not be invocable through a const Database - write methods are non-const");
static_assert(CanFetchAllThroughConst<Person>::value,
"FetchAll must remain invocable through a const Database - read methods are const");

class DatabaseTest : public ::testing::Test {
void SetUp() override {
Database::Initialize("");
Expand All @@ -41,6 +77,19 @@ class DatabaseTest : public ::testing::Test {
}
};

TEST_F(DatabaseTest, ReadOnlyHandleCanStillFetch) {
const auto db = Database::Instance();
const Person p{L"ada", L"lovelace", 36, true, 1};
db->Save(p);

// The implicit shared_ptr<Database> -> shared_ptr<const Database> conversion yields a
// read-only view, through which the const fetch methods keep working
std::shared_ptr<const Database> reader = Database::Instance();
const auto all = reader->FetchAll<Person>();
ASSERT_EQ(1, all.size());
EXPECT_EQ(p.first_name, all[0].first_name);
}

TEST_F(DatabaseTest, Initialization) {
const auto db = Database::Instance();

Expand Down
Loading