diff --git a/IDHANServer/src/api/tags/createTag.cpp b/IDHANServer/src/api/tags/createTag.cpp index 9dfda1a..db574c6 100644 --- a/IDHANServer/src/api/tags/createTag.cpp +++ b/IDHANServer/src/api/tags/createTag.cpp @@ -6,92 +6,222 @@ #include #include -namespace std -{ +#include "api/TagAPI.hpp" +#include "api/helpers/ExpectedTask.hpp" +#include "api/helpers/createBadRequest.hpp" +#include "db/drogonArrayBind.hpp" +#include "fgl/defines.hpp" +#include "logging/ScopedTimer.hpp" + template <> -struct hash< std::pair< std::string, std::string > > +struct std::hash< std::pair< std::string, std::string > > { std::size_t operator()( const std::pair< std::string, std::string >& p ) const noexcept { return std::hash< std::string > {}( p.first + ":" + p.second ); } }; -} // namespace std -#include "api/TagAPI.hpp" -#include "api/helpers/createBadRequest.hpp" -#include "db/drogonArrayBind.hpp" -#include "fgl/defines.hpp" -#include "logging/ScopedTimer.hpp" +template <> +struct std::hash< std::pair< idhan::NamespaceID, idhan::SubtagID > > +{ + std::size_t operator()( const std::pair< idhan::NamespaceID, idhan::SubtagID >& p ) const noexcept + { + static_assert( + ( sizeof( idhan::NamespaceID ) == sizeof( std::uint32_t ) ) + && ( sizeof( idhan::SubtagID ) == sizeof( std::uint32_t ) ), + "pair hash of NamespaceID and SubtagID only works with 32 bit, If you get this error, you need to write a better hasher" ); + + std::size_t hash { 0 }; + hash = p.second; + hash |= static_cast< std::size_t >( p.first ) << 32; + + return hash; + } +}; namespace idhan::api { +drogon::Task< std::unordered_map< std::string, NamespaceID > > getNamespaces( + std::set< std::string > namespace_set, + DbClientPtr db ) +{ + std::unordered_map< std::string, NamespaceID > map {}; + + std::vector< std::string > namespace_texts {}; + std::ranges::copy( namespace_set, std::back_inserter( namespace_texts ) ); + + auto namespace_selection { co_await db->execSqlCoro( + "SELECT namespace_id, namespace_text FROM tag_namespaces WHERE namespace_text = ANY($1::TEXT[])", + std::forward< std::vector< std::string > >( namespace_texts ) ) }; + + if ( namespace_selection.size() != namespace_set.size() ) + { + // do an insertion + co_await db->execSqlCoro( + "INSERT INTO tag_namespaces (namespace_text) VALUES (UNNEST($1::TEXT[])) ON CONFLICT DO NOTHING", + std::forward< std::vector< std::string > >( namespace_texts ) ); + + // select again + namespace_selection = co_await db->execSqlCoro( + "SELECT namespace_id, namespace_text FROM tag_namespaces WHERE namespace_text = ANY($1::TEXT[])", + std::forward< std::vector< std::string > >( namespace_texts ) ); + } + + for ( const auto& row : namespace_selection ) + { + const auto namespace_id { row[ 0 ].as< NamespaceID >() }; + const auto namespace_text { row[ 1 ].as< std::string >() }; + map.emplace( namespace_text, namespace_id ); + } + + co_return map; +} + +drogon::Task< std::unordered_map< std::string, SubtagID > > getSubtags( + std::set< std::string > subtag_set, + DbClientPtr db ) +{ + std::unordered_map< std::string, SubtagID > map {}; + + std::vector< std::string > subtag_texts {}; + std::ranges::copy( subtag_set, std::back_inserter( subtag_texts ) ); + + auto subtag_selection { co_await db->execSqlCoro( + "SELECT subtag_id, subtag_text FROM tag_subtags WHERE subtag_text = ANY($1::TEXT[])", + std::forward< std::vector< std::string > >( subtag_texts ) ) }; + + if ( subtag_selection.size() != subtag_set.size() ) + { + co_await db->execSqlCoro( + "INSERT INTO tag_subtags (subtag_text) VALUES (UNNEST($1::TEXT[])) ON CONFLICT DO NOTHING", + std::forward< std::vector< std::string > >( subtag_texts ) ); + + subtag_selection = co_await db->execSqlCoro( + "SELECT subtag_id, subtag_text FROM tag_subtags WHERE subtag_text = ANY($1::TEXT[])", + std::forward< std::vector< std::string > >( subtag_texts ) ); + } + + for ( const auto& row : subtag_selection ) + { + const auto subtag_id { row[ 0 ].as< SubtagID >() }; + const auto subtag_text { row[ 1 ].as< std::string >() }; + map.emplace( subtag_text, subtag_id ); + } + + co_return map; +} + +drogon::Task< std::unordered_map< std::pair< NamespaceID, SubtagID >, TagID > > getTags( + std::vector< NamespaceID > namespace_ids, + std::vector< SubtagID > subtag_ids, + DbClientPtr db ) +{ + std::unordered_map< std::pair< NamespaceID, SubtagID >, TagID > map {}; + + map.reserve( namespace_ids.size() ); + + const auto select_result { co_await db->execSqlCoro( + "WITH t(namespace_id, subtag_id) AS (SELECT * FROM UNNEST($1::" NAMESPACE_ID_PG_TYPE_NAME + "[], $2::" SUBTAG_ID_PG_TYPE_NAME + "[])) SELECT tag_id, namespace_id, subtag_id FROM t JOIN tags USING (namespace_id, subtag_id)", + std::forward< std::vector< NamespaceID > >( namespace_ids ), + std::forward< std::vector< SubtagID > >( subtag_ids ) ) }; + + if ( select_result.size() != namespace_ids.size() ) + { + const auto new_tag_ids { co_await db->execSqlCoro( + "INSERT INTO tags (namespace_id, subtag_id) VALUES (UNNEST($1::" NAMESPACE_ID_PG_TYPE_NAME + "[]), UNNEST($2::" SUBTAG_ID_PG_TYPE_NAME + "[])) ON CONFLICT DO NOTHING RETURNING tag_id, namespace_id, subtag_id", + std::forward< std::vector< NamespaceID > >( namespace_ids ), + std::forward< std::vector< SubtagID > >( subtag_ids ) ) }; + + for ( const auto& row : new_tag_ids ) + { + const auto tag_id { row[ 0 ].as< TagID >() }; + FGL_ASSERT( tag_id > 0, "Tag ID was not greater then zero!" ); + const auto namespace_id { row[ 1 ].as< NamespaceID >() }; + const auto subtag_id { row[ 2 ].as< SubtagID >() }; + map.emplace( std::make_pair( namespace_id, subtag_id ), tag_id ); + } + } + + for ( const auto& row : select_result ) + { + // Tag was created in the previous step, so skip it + if ( row[ 0 ].isNull() ) continue; + const auto tag_id { row[ 0 ].as< TagID >() }; + FGL_ASSERT( tag_id > 0, "Tag ID was not greater then zero!" ); + const auto namespace_id { row[ 1 ].as< NamespaceID >() }; + const auto subtag_id { row[ 2 ].as< SubtagID >() }; + map.emplace( std::make_pair( namespace_id, subtag_id ), tag_id ); + } + + co_return map; +} + drogon::Task< std::expected< std::vector< TagID >, drogon::HttpResponsePtr > > createTagsFromPairs( const std::vector< std::pair< std::string, std::string > >& tag_pairs, const DbClientPtr db ) { logging::ScopedTimer timer { "createTags" }; - std::vector< TagID > tag_ids {}; - tag_ids.reserve( tag_pairs.size() ); - - std::vector< std::string > namespace_params {}; - namespace_params.reserve( tag_pairs.size() ); - std::vector< std::string > subtag_params {}; - subtag_params.reserve( tag_pairs.size() ); - - for ( const auto& [ namespace_text, subtag_text ] : tag_pairs ) - { - namespace_params.emplace_back( namespace_text ); - subtag_params.emplace_back( subtag_text ); - } - - static std::binary_semaphore sem { 1 }; - if ( tag_pairs.empty() ) { co_return std::unexpected( createBadRequest( "No tags to create" ) ); } - try + std::vector< TagID > tag_ids {}; + tag_ids.reserve( tag_pairs.size() ); + + std::set< std::string > namespace_set {}; + std::set< std::string > subtag_set {}; + + for ( const auto& [ namespace_text, subtag_text ] : tag_pairs ) { - sem.acquire(); - - const auto result { co_await db->execSqlCoro( - "SELECT tag_id FROM createBatchTags($1::TEXT[], $2::TEXT[])", - std::move( namespace_params ), - std::move( subtag_params ) ) }; - - for ( const auto& row : result ) - { - const auto& tag_id { row[ "tag_id" ].as< TagID >() }; - - if ( !( tag_id > 0 ) ) [[unlikely]] - co_return std::unexpected( - createInternalError( "Failed to create tag, got {}. Expected tag_id > 0", tag_id ) ); - - tag_ids.emplace_back( tag_id ); - } - - if ( tag_ids.size() != tag_pairs.size() ) - { - sem.release(); - co_return std::unexpected( createInternalError( - "Failed to create tags. Count mismatch Expected {} got {} ", tag_pairs.size(), tag_ids.size() ) ); - } - - sem.release(); - co_return tag_ids; - } - catch ( std::exception& e ) - { - sem.release(); - co_return std::unexpected( createInternalError( "Failed to create tags: {}", e.what() ) ); + namespace_set.emplace( namespace_text ); + subtag_set.emplace( subtag_text ); } - sem.release(); - co_return std::unexpected( createInternalError( "Failed to create tags" ) ); + std::unordered_map< std::string, NamespaceID > namespace_map { co_await getNamespaces( namespace_set, db ) }; + std::unordered_map< std::string, SubtagID > subtag_map { co_await getSubtags( subtag_set, db ) }; + + std::vector< NamespaceID > namespace_ids {}; + std::vector< SubtagID > subtag_ids {}; + + for ( const auto& [ namespace_text, subtag_text ] : tag_pairs ) + { + const auto namespace_id { namespace_map.at( namespace_text ) }; + const auto subtag_id { subtag_map.at( subtag_text ) }; + + namespace_ids.emplace_back( namespace_id ); + subtag_ids.emplace_back( subtag_id ); + } + + std::unordered_map< std::pair< NamespaceID, SubtagID >, TagID > tag_map { + co_await getTags( namespace_ids, subtag_ids, db ) + }; + + if ( tag_map.size() != namespace_ids.size() ) + { + co_return std::unexpected( createBadRequest( "Tag count mismatch" ) ); + } + + for ( const auto& [ namespace_text, subtag_text ] : tag_pairs ) + { + const auto namespace_id { namespace_map.at( namespace_text ) }; + const auto subtag_id { subtag_map.at( subtag_text ) }; + + const auto tag_id { tag_map.at( std::make_pair( namespace_id, subtag_id ) ) }; + + FGL_ASSERT( tag_id > 0, "Tag ID was not valid" ); + + tag_ids.emplace_back( tag_id ); + } + + co_return tag_ids; } drogon::Task< drogon::HttpResponsePtr > TagAPI::createTagsFromRequest( const drogon::HttpRequestPtr request )