add src/ikarus subdir and make names unique for objects per scope

Signed-off-by: Folling <mail@folling.io>
This commit is contained in:
Folling 2024-01-30 09:31:08 +01:00 committed by Folling
parent c65211e4ff
commit 5dce2ced94
No known key found for this signature in database
51 changed files with 590 additions and 735 deletions

View file

@ -0,0 +1,40 @@
#pragma once
#include <array>
#include <cppbase/assets.hpp>
#include <cppbase/result.hpp>
#include <sqlitecpp/connection.hpp>
namespace ikarus {
CPPBASE_ASSET(m0_initial_layout, "ikarus/persistence/migrations/m0_initial_layout.sql");
class Migration : public sqlitecpp::Migration {
public:
Migration(char const * sql, size_t size):
sql{sql, size} {}
~Migration() override = default;
public:
[[nodiscard]] inline auto get_content() const -> std::string_view override {
return sql;
}
public:
std::string_view sql;
};
#define DECLARE_MIGRATION(name) std::make_unique<ikarus::Migration>(static_cast<char const *>(name()), name##_size())
constexpr std::string_view DB_VERSION_KEY = "IKARUS_DB_VERSION";
std::vector<std::unique_ptr<sqlitecpp::Migration>> const MIGRATIONS = []() {
std::vector<std::unique_ptr<sqlitecpp::Migration>> ret;
ret.emplace_back(DECLARE_MIGRATION(m0_initial_layout));
return ret;
}();
} // namespace ikarus

View file

@ -0,0 +1,68 @@
CREATE TABLE `objects`
(
`do_not_access_rowid_alias` INTEGER PRIMARY KEY,
`type` INT NOT NULL,
`id` INT GENERATED ALWAYS AS (`do_not_access_rowid_alias` | (`type` << 56)) VIRTUAL UNIQUE
) STRICT;
CREATE UNIQUE INDEX `object_id` ON `objects` (`id`);
CREATE INDEX `object_type` ON `objects` (`type`);
CREATE TABLE `entities`
(
`id` INT,
`name` TEXT NOT NULL,
PRIMARY KEY (`id`),
UNIQUE (`name`),
FOREIGN KEY (`id`) REFERENCES `objects` (`id`) ON DELETE CASCADE
) WITHOUT ROWID, STRICT;
CREATE TABLE `blueprints`
(
`id` INT,
`name` TEXT NOT NULL,
PRIMARY KEY (`id`),
UNIQUE (`name`),
FOREIGN KEY (`id`) REFERENCES `objects` (`id`) ON DELETE CASCADE
) WITHOUT ROWID, STRICT;
CREATE TABLE `entity_blueprint_links`
(
`entity` INT NOT NULL,
`blueprint` INT NOT NULL,
PRIMARY KEY (`entity`, `blueprint`),
FOREIGN KEY (`entity`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`blueprint`) REFERENCES `blueprints` (`id`) ON DELETE CASCADE
) WITHOUT ROWID, STRICT;
CREATE TABLE `properties`
(
`id` INT,
`name` TEXT NOT NULL,
`type` INT NOT NULL,
`default_value` TEXT NOT NULL,
`settings` TEXT NOT NULL,
`source` INT NOT NULL,
PRIMARY KEY (`id`),
UNIQUE(`source`, `name`),
FOREIGN KEY (`id`) REFERENCES `objects` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`source`) REFERENCES `objects` (`id`) ON DELETE CASCADE
) WITHOUT ROWID, STRICT;
CREATE INDEX `properties_type` ON `properties` (`type`);
CREATE INDEX `properties_source` ON `properties` (`source`);
CREATE TABLE `values`
(
`entity` INT NOT NULL,
`property` INT NOT NULL,
`value` TEXT NOT NULL,
PRIMARY KEY (`entity`, `property`),
FOREIGN KEY (`entity`) REFERENCES `entities` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`property`) REFERENCES `properties` (`id`) ON DELETE CASCADE
) WITHOUT ROWID, STRICT;

View file

@ -0,0 +1,277 @@
#include "ikarus/persistence/project.h"
#include <boost/filesystem.hpp>
#include <cppbase/strings.hpp>
#include <ikarus/objects/blueprint.hpp>
#include <ikarus/objects/entity.hpp>
#include <ikarus/objects/properties/number_property.hpp>
#include <ikarus/objects/properties/property.hpp>
#include <ikarus/objects/properties/text_property.hpp>
#include <ikarus/objects/properties/toggle_property.hpp>
#include <ikarus/persistence/migrations.hpp>
#include <ikarus/persistence/project.hpp>
IkarusProject::IkarusProject(std::string_view name, std::string_view path, std::unique_ptr<sqlitecpp::Connection> && db):
name{name},
path{std::move(path)},
db{std::move(db)},
_blueprints{},
_properties{},
_entities{} {}
IkarusBlueprint * IkarusProject::get_blueprint(IkarusId id) {
return get_cached_object<IkarusBlueprint>(id, this->_blueprints);
}
auto IkarusProject::uncache(IkarusBlueprint * blueprint) -> void {
remove_cached_object(blueprint, _blueprints);
}
auto IkarusProject::get_entity(IkarusId id) -> IkarusEntity * {
return get_cached_object<IkarusEntity>(id, this->_entities);
}
auto IkarusProject::uncache(IkarusEntity * entity) -> void {
remove_cached_object(entity, _entities);
}
auto IkarusProject::get_property(IkarusId id, IkarusPropertyType type) -> IkarusProperty * {
auto const iter = _properties.find(id);
if (iter == _properties.cend()) {
switch (type) {
case IkarusPropertyType_Toggle:
return _properties.emplace(id, std::make_unique<IkarusToggleProperty>(this, id)).first->second.get();
case IkarusPropertyType_Number:
return _properties.emplace(id, std::make_unique<IkarusNumberProperty>(this, id)).first->second.get();
case IkarusPropertyType_Text: return _properties.emplace(id, std::make_unique<IkarusTextProperty>(this, id)).first->second.get();
}
}
return iter->second.get();
}
auto IkarusProject::uncache(IkarusProperty * property) -> void {
remove_cached_object(property, _properties);
}
IkarusProject * ikarus_project_create(char const * path, char const * name, IkarusErrorData * error_out) {
IKARUS_FAIL_IF_NULL(path, nullptr);
IKARUS_FAIL_IF_NULL(name, nullptr);
IKARUS_FAIL_IF(cppbase::is_empty_or_blank(path), nullptr, "path must not be empty", IkarusErrorInfo_Client_InvalidInput);
IKARUS_FAIL_IF(cppbase::is_empty_or_blank(name), nullptr, "name must not be empty", IkarusErrorInfo_Client_InvalidInput);
boost::filesystem::path fs_path{path};
{
boost::system::error_code ec;
bool const exists = fs::exists(fs_path, ec);
IKARUS_FAIL_IF(ec, nullptr, "unable to check whether path is occupied", IkarusErrorInfo_Filesystem_AlreadyExists);
IKARUS_FAIL_IF(exists, nullptr, "path is already occupied", IkarusErrorInfo_Filesystem_AlreadyExists);
}
IKARUS_VTRYRV_OR_FAIL(
auto db,
nullptr,
"failed to create project db: {}",
IkarusErrorInfo_Database_ConnectionFailed,
sqlitecpp::Connection::open(path)
);
IKARUS_TRYRV_OR_FAIL(
nullptr,
"failed to migrate project db: {}",
IkarusErrorInfo_Database_MigrationFailed,
db->migrate(ikarus::MIGRATIONS)
);
if (auto res = db->execute("INSERT INTO `metadata`(`key`, `value`) VALUES(?, ?)", DB_PROJECT_NAME_KEY, name); res.is_error()) {
boost::system::error_code ec;
fs::remove(fs_path, ec);
IKARUS_FAIL_IF(
ec,
nullptr,
"failed to remove project db after being unable to insert name into metadata table: {}",
IkarusErrorInfo_Filesystem_AccessIssue
);
IKARUS_FAIL(nullptr, "failed to insert project name into metadata: {}", IkarusErrorInfo_Database_QueryFailed);
}
return new IkarusProject{name, path, std::move(db)};
}
IkarusProject * ikarus_project_create_in_memory(char const * name, IkarusErrorData * error_out) {
IKARUS_FAIL_IF_NULL(name, nullptr);
IKARUS_FAIL_IF(cppbase::is_empty_or_blank(name), nullptr, "name must not be empty", IkarusErrorInfo_Client_InvalidInput);
IKARUS_VTRYRV_OR_FAIL(
auto db,
nullptr,
"failed to create project db in memory: {}",
IkarusErrorInfo_Database_ConnectionFailed,
sqlitecpp::Connection::open_in_memory()
);
IKARUS_TRYRV_OR_FAIL(
nullptr,
"failed to migrate project db: {}",
IkarusErrorInfo_Database_MigrationFailed,
db->migrate(ikarus::MIGRATIONS)
);
IKARUS_TRYRV_OR_FAIL(
nullptr,
"failed to insert project name into metadata: {}",
IkarusErrorInfo_Database_QueryFailed,
db->execute("INSERT INTO `metadata`(`key`, `value`) VALUES(?, ?)", DB_PROJECT_NAME_KEY, name)
);
return new IkarusProject{name, ":memory:", std::move(db)};
}
IkarusProject * ikarus_project_open(char const * path, IkarusErrorData * error_out) {
IKARUS_FAIL_IF_NULL(path, nullptr);
IKARUS_FAIL_IF(cppbase::is_empty_or_blank(path), nullptr, "path must not be empty", IkarusErrorInfo_Client_InvalidInput);
IKARUS_VTRYRV_OR_FAIL(
auto db,
nullptr,
"failed to open project db: {}",
IkarusErrorInfo_Database_ConnectionFailed,
sqlitecpp::Connection::open(path)
);
IKARUS_TRYRV_OR_FAIL(
nullptr,
"failed to migrate project db: {}",
IkarusErrorInfo_Database_MigrationFailed,
db->migrate(ikarus::MIGRATIONS)
);
IKARUS_VTRYRV_OR_FAIL(
auto const & name,
nullptr,
"failed to retrieve project name from metadata: {}",
IkarusErrorInfo_Database_QueryFailed,
db->query_one<std::string>("SELECT `value` FROM `metadata` WHERE `key` = ?", DB_PROJECT_NAME_KEY)
);
return new IkarusProject{name, path, std::move(db)};
}
char const * ikarus_project_get_name(IkarusProject const * project, IkarusErrorData * error_out) {
IKARUS_FAIL_IF_NULL(project, nullptr);
return project->name.data();
}
void ikarus_project_set_name(IkarusProject * project, char const * new_name, IkarusErrorData * error_out) {
IKARUS_FAIL_IF_NULL(project, );
IKARUS_FAIL_IF_NULL(new_name, );
IKARUS_FAIL_IF(cppbase::is_empty_or_blank(new_name), , "name must not be empty", IkarusErrorInfo_Client_InvalidInput);
IKARUS_TRYRV_OR_FAIL(
,
"failed to update project name: {}",
IkarusErrorInfo_Database_QueryFailed,
project->db->execute("UPDATE `metadata` SET `value` = ? WHERE `key` = ?", new_name, DB_PROJECT_NAME_KEY)
);
}
char const * ikarus_project_get_path(IkarusProject const * project, IkarusErrorData * error_out) {
IKARUS_FAIL_IF_NULL(project, nullptr);
return project->path.data();
}
// these take a mutable project right now because the get_cached-* function must be mutable
// since we insert a backreference to the project into the objects, not ideal,
// but fine for now since "mutability" is a vague concept for projects anyway
void ikarus_project_get_blueprints(
IkarusProject * project,
struct IkarusBlueprint ** blueprints_out,
size_t blueprints_out_size,
IkarusErrorData * error_out
) {
IKARUS_FAIL_IF_NULL(project, );
IKARUS_FAIL_IF_NULL(blueprints_out, );
if (blueprints_out_size == 0) {
return;
}
IkarusId ids[blueprints_out_size];
IKARUS_TRYRV_OR_FAIL(
,
"unable to fetch project blueprints from database: {}",
IkarusErrorInfo_Database_QueryFailed,
project->db->query_many_buffered<IkarusId>("SELECT `id` FROM `blueprints`", ids, blueprints_out_size)
);
for (size_t i = 0; i < blueprints_out_size; ++i) {
blueprints_out[i] = project->get_blueprint(ids[i]);
}
}
size_t ikarus_project_get_blueprint_count(IkarusProject const * project, IkarusErrorData * error_out) {
IKARUS_FAIL_IF_NULL(project, 0);
IKARUS_VTRYRV_OR_FAIL(
auto const ret,
0,
"unable to fetch project blueprint count from database: {}",
IkarusErrorInfo_Database_QueryFailed,
project->db->query_one<int64_t>("SELECT COUNT(*) FROM `blueprints`")
);
return ret;
}
void ikarus_project_get_entities(
IkarusProject * project,
struct IkarusEntity ** entities_out,
size_t entities_out_size,
IkarusErrorData * error_out
) {
IKARUS_FAIL_IF_NULL(project, );
IKARUS_FAIL_IF_NULL(entities_out, );
if (entities_out_size == 0) {
return;
}
IkarusId ids[entities_out_size];
IKARUS_TRYRV_OR_FAIL(
,
"unable to fetch project entities from database: {}",
IkarusErrorInfo_Database_QueryFailed,
project->db->query_many_buffered<IkarusId>("SELECT `id` FROM `entities`", ids, entities_out_size)
);
for (size_t i = 0; i < entities_out_size; ++i) {
entities_out[i] = project->get_entity(ids[i]);
}
}
size_t ikarus_project_get_entity_count(IkarusProject * project, IkarusErrorData * error_out) {
IKARUS_FAIL_IF_NULL(project, 0);
IKARUS_VTRYRV_OR_FAIL(
auto const ret,
0,
"unable to fetch project entity count from database: {}",
IkarusErrorInfo_Database_QueryFailed,
project->db->query_one<int64_t>("SELECT COUNT(*) FROM `entities`")
);
return ret;
}

View file

@ -0,0 +1,61 @@
#pragma once
#include <ranges>
#include <stack>
#include <string>
#include <boost/filesystem.hpp>
#include <sqlitecpp/connection.hpp>
#include <ikarus/errors.h>
#include <ikarus/id.h>
#include <ikarus/objects/properties/property_type.h>
namespace fs = boost::filesystem;
/// \private
struct IkarusProject {
public:
IkarusProject(std::string_view name, std::string_view path, std::unique_ptr<sqlitecpp::Connection> && db);
public:
[[nodiscard]] auto get_blueprint(IkarusId id) -> struct IkarusBlueprint *;
auto uncache(struct IkarusBlueprint * blueprint) -> void;
[[nodiscard]] auto get_entity(IkarusId id) -> struct IkarusEntity *;
auto uncache(struct IkarusEntity * entity) -> void;
// TODO improve this to take a template param so that we don't have to cast in e.g. ikarus_toggle_property_create
[[nodiscard]] auto get_property(IkarusId id, IkarusPropertyType type) -> struct IkarusProperty *;
auto uncache(struct IkarusProperty * property) -> void;
private:
template<typename T>
[[nodiscard]] T * get_cached_object(IkarusId id, auto & cache) {
auto const iter = cache.find(id);
if (iter == cache.cend()) {
return cache.emplace(id, std::make_unique<T>(this, id)).first->second.get();
}
return iter->second.get();
}
template<typename T>
void remove_cached_object(T * object, std::unordered_map<IkarusId, std::unique_ptr<T>> & cache) {
cache.erase(object->id);
}
public:
std::string name;
std::string_view path;
std::unique_ptr<sqlitecpp::Connection> db;
private:
std::unordered_map<IkarusId, std::unique_ptr<struct IkarusBlueprint>> mutable _blueprints;
std::unordered_map<IkarusId, std::unique_ptr<struct IkarusProperty>> mutable _properties;
std::unordered_map<IkarusId, std::unique_ptr<struct IkarusEntity>> mutable _entities;
};
constexpr std::string_view DB_PROJECT_NAME_KEY = "PROJECT_NAME";