340 lines
11 KiB
C++
340 lines
11 KiB
C++
#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/objects/util.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,
|
|
fmt::format("unable to check whether path is occupied: {}", ec.message()),
|
|
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 const * 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;
|
|
}
|
|
|
|
struct IkarusEntity * get_entity_by_name(IkarusProject * project, char const * name, IkarusErrorData * error_out) {
|
|
IKARUS_FAIL_IF_NULL(project, nullptr);
|
|
IKARUS_FAIL_IF_NULL(name, nullptr);
|
|
IKARUS_FAIL_IF_NAME_INVALID(name, nullptr);
|
|
|
|
// TODO, 'InvalidInput' doesn't really make sense here, we'd need to adjust the macros to support distinguishing between different
|
|
// errors. In this case `sqlitecpp::MissingRow` and database related errors. Same for the other functions.
|
|
IKARUS_VTRYRV_OR_FAIL(
|
|
auto const id,
|
|
nullptr,
|
|
"unable to find entity in database: {}",
|
|
IkarusErrorInfo_Client_InvalidInput,
|
|
project->db->query_one<IkarusId>("SELECT `id` FROM `entities` WHERE `name` = ?", name)
|
|
);
|
|
|
|
return project->get_entity(id);
|
|
}
|
|
|
|
struct IkarusProperty *
|
|
get_property_by_name(IkarusProject * project, IkarusPropertyScope * scope, char const * name, IkarusErrorData * error_out) {
|
|
IKARUS_FAIL_IF_NULL(project, nullptr);
|
|
IKARUS_FAIL_IF_NULL(name, nullptr);
|
|
IKARUS_FAIL_IF_NAME_INVALID(name, nullptr);
|
|
|
|
IKARUS_VTRYRV_OR_FAIL(
|
|
auto const id_and_type,
|
|
nullptr,
|
|
"unable to find property in database: {}",
|
|
IkarusErrorInfo_Client_InvalidInput,
|
|
project->db->query_one<IkarusId, IkarusPropertyType>(
|
|
"SELECT `id`, `type` FROM `properties` WHERE `name` = ? AND `scope` = ?",
|
|
name,
|
|
scope->get_id()
|
|
)
|
|
);
|
|
|
|
auto const [id, type] = id_and_type;
|
|
|
|
return project->get_property(id, type);
|
|
}
|
|
|
|
IkarusBlueprint * get_blueprints_by_name(IkarusProject * project, char const * name, IkarusErrorData * error_out) {
|
|
IKARUS_FAIL_IF_NULL(project, nullptr);
|
|
IKARUS_FAIL_IF_NULL(name, nullptr);
|
|
IKARUS_FAIL_IF_NAME_INVALID(name, nullptr);
|
|
|
|
IKARUS_VTRYRV_OR_FAIL(
|
|
auto const id,
|
|
nullptr,
|
|
"unable to find blueprint in database: {}",
|
|
IkarusErrorInfo_Client_InvalidInput,
|
|
project->db->query_one<IkarusId>("SELECT `id` FROM `blueprints` WHERE `name` = ?", name)
|
|
);
|
|
|
|
return project->get_blueprint(id);
|
|
}
|