#include "ikarus/persistence/project.h" #include #include #include #include #include #include #include #include #include #include #include IkarusProject::IkarusProject(std::string_view name, std::string_view path, std::unique_ptr && db): name{name}, path{std::move(path)}, db{std::move(db)}, _blueprints{}, _properties{}, _entities{} {} IkarusBlueprint * IkarusProject::get_blueprint(IkarusId id) { return get_cached_object(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(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(this, id)).first->second.get(); case IkarusPropertyType_Number: return _properties.emplace(id, std::make_unique(this, id)).first->second.get(); case IkarusPropertyType_Text: return _properties.emplace(id, std::make_unique(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("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("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("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("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("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("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( "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("SELECT `id` FROM `blueprints` WHERE `name` = ?", name) ); return project->get_blueprint(id); }