Refactor tag creation to use helper lookup functions (getNamespaces, getSubtags, getTags) and remove the semaphore for better concurrency and error handling.
This commit is contained in:
@@ -6,92 +6,222 @@
|
||||
#include <functional>
|
||||
#include <ranges>
|
||||
|
||||
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 )
|
||||
|
||||
Reference in New Issue
Block a user