From df07023cfeac9d4a18045b7e33f079daaaa69b3a Mon Sep 17 00:00:00 2001 From: kj16609 Date: Sun, 24 Aug 2025 16:29:54 -0400 Subject: [PATCH] Rework migrations system and tag relationships --- .gitignore | 4 +- IDHANMigration/CMakeLists.txt | 4 - IDHANMigration/include/management.hpp | 9 -- IDHANMigration/src/00-idhan_info.sql | 3 +- .../75-func_tagAliasesAfterUpdateTrigger.sql | 5 + .../src/90-active_tag_mappings_parents.sql | 2 +- .../src/91-func_activeTagParents.sql | 73 ++++++++-- IDHANMigration/src/93-tag_counts.sql | 46 +++++++ .../src/94-active_mappings_internal.sql | 42 +++--- IDHANMigration/src/management.cpp | 22 --- IDHANServer/CMakeLists.txt | 5 - IDHANServer/src/api/tags/autocompleteTag.cpp | 13 +- IDHANServer/src/db/ManagementConnection.cpp | 8 -- IDHANServer/src/db/ManagementConnection.hpp | 2 - tests/src/server/tags/MappingFixture.cpp | 20 +-- tests/src/server/tags/MappingFixture.hpp | 6 +- tests/src/server/tags/ServerTagFixture.cpp | 127 ++++++++++++++++++ tests/src/server/tags/ServerTagFixture.hpp | 6 + tests/src/server/tags/mappings.cpp | 10 +- tests/src/server/tags/parents.cpp | 35 +++++ 20 files changed, 335 insertions(+), 107 deletions(-) create mode 100644 tests/src/server/tags/parents.cpp diff --git a/.gitignore b/.gitignore index f26e05e..bfda4ca 100644 --- a/.gitignore +++ b/.gitignore @@ -35,5 +35,5 @@ read.lock /IDHAN/include/hydrus_client_constants_gen.hpp # Used for allowing easy setup of development environments for testing -/dev.sql -dev.sql \ No newline at end of file +IDHANServer/src/db/dev.sql +IDHANServer/src/db/pre-dev.sql diff --git a/IDHANMigration/CMakeLists.txt b/IDHANMigration/CMakeLists.txt index 6b66383..203c10e 100644 --- a/IDHANMigration/CMakeLists.txt +++ b/IDHANMigration/CMakeLists.txt @@ -20,7 +20,3 @@ file(GLOB_RECURSE CPP_SOURCES CONFIGURE_DEPENDS add_library(IDHANMigration STATIC ${MIGRATION_SOURCE} ${CPP_SOURCES}) target_include_directories(IDHANMigration PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_link_libraries(IDHANMigration PRIVATE pqxx spdlog::spdlog) - -if (DEFINED ALLOW_TABLE_DESTRUCTION AND ALLOW_TABLE_DESTRUCTION) - target_compile_definitions(IDHANMigration PUBLIC ALLOW_TABLE_DESTRUCTION=1) -endif () diff --git a/IDHANMigration/include/management.hpp b/IDHANMigration/include/management.hpp index 6cc67e3..63e3ecd 100644 --- a/IDHANMigration/include/management.hpp +++ b/IDHANMigration/include/management.hpp @@ -22,13 +22,4 @@ std::uint16_t getTableVersion( pqxx::nontransaction& tx, std::string_view name ) void addTableToInfo( pqxx::nontransaction& tx, std::string_view name, std::string_view creation_query, std::size_t migration_id ); -#ifdef ALLOW_TABLE_DESTRUCTION -void destroyTables( pqxx::nontransaction& ); -#else -inline void destroyTables( [[maybe_unused]] pqxx::nontransaction& ) -{ - return; -} -#endif - } // namespace idhan::db \ No newline at end of file diff --git a/IDHANMigration/src/00-idhan_info.sql b/IDHANMigration/src/00-idhan_info.sql index b080b9e..b07a958 100644 --- a/IDHANMigration/src/00-idhan_info.sql +++ b/IDHANMigration/src/00-idhan_info.sql @@ -5,4 +5,5 @@ CREATE TABLE idhan_info queries TEXT[] NOT NULL ); -CREATE EXTENSION IF NOT EXISTS pgcrypto; \ No newline at end of file +CREATE EXTENSION IF NOT EXISTS pgcrypto; +CREATE EXTENSION IF NOT EXISTS pg_trgm; \ No newline at end of file diff --git a/IDHANMigration/src/75-func_tagAliasesAfterUpdateTrigger.sql b/IDHANMigration/src/75-func_tagAliasesAfterUpdateTrigger.sql index a846b73..fb17b4c 100644 --- a/IDHANMigration/src/75-func_tagAliasesAfterUpdateTrigger.sql +++ b/IDHANMigration/src/75-func_tagAliasesAfterUpdateTrigger.sql @@ -6,6 +6,11 @@ AS $$ BEGIN + IF EXISTS(SELECT 1 FROM tag_aliases ta WHERE COALESCE(ta.ideal_alias_id, ta.alias_id) = new.aliased_id AND ta.tag_domain_id = new.tag_domain_id AND ta.aliased_id <> COALESCE(new.ideal_alias_id, new.alias_id)) + THEN + RAISE EXCEPTION 'Recursive alias detected during update'; + END IF; + UPDATE tag_aliases SET ideal_alias_id = COALESCE(new.ideal_alias_id, new.alias_id) WHERE tag_aliases.alias_id = new.aliased_id diff --git a/IDHANMigration/src/90-active_tag_mappings_parents.sql b/IDHANMigration/src/90-active_tag_mappings_parents.sql index f86ab03..7d5b3b8 100644 --- a/IDHANMigration/src/90-active_tag_mappings_parents.sql +++ b/IDHANMigration/src/90-active_tag_mappings_parents.sql @@ -5,6 +5,6 @@ CREATE TABLE active_tag_mappings_parents origin_id BIGINT REFERENCES tags (tag_id) NOT NULL, tag_domain_id SMALLINT REFERENCES tag_domains (tag_domain_id) NOT NULL, internal BOOLEAN DEFAULT FALSE NOT NULL, - internal_count INTEGER DEFAULT 0, + internal_count INTEGER DEFAULT 0 NOT NULL, PRIMARY KEY (record_id, tag_id, origin_id, tag_domain_id) ); \ No newline at end of file diff --git a/IDHANMigration/src/91-func_activeTagParents.sql b/IDHANMigration/src/91-func_activeTagParents.sql index 97f2a80..aae2415 100644 --- a/IDHANMigration/src/91-func_activeTagParents.sql +++ b/IDHANMigration/src/91-func_activeTagParents.sql @@ -9,9 +9,25 @@ BEGIN WHERE tm.tag_domain_id = new.tag_domain_id AND tm.tag_id = new.child_id; + INSERT INTO active_tag_mappings_parents (record_id, tag_id, origin_id, tag_domain_id, internal, internal_count) + SELECT DISTINCT atmp.record_id, + new.parent_id, + new.child_id, + new.tag_domain_id, + TRUE AS internal, + (SELECT COUNT(*) + FROM active_tag_mappings_parents atmp_count + WHERE atmp_count.tag_domain_id = new.tag_domain_id + AND atmp_count.tag_id = new.child_id + AND atmp_count.record_id = atmp.record_id) AS internal_count + FROM active_tag_mappings_parents atmp + WHERE atmp.tag_domain_id = new.tag_domain_id + AND atmp.tag_id = new.child_id + ON CONFLICT (record_id, tag_id, origin_id, tag_domain_id) DO UPDATE SET internal_count = excluded.internal_count + 1; + RETURN new; END; -$$ LANGUAGE plpgsql; +$$ LANGUAGE plpgsql VOLATILE; -- Create the trigger CREATE TRIGGER trg_insert_active_tag_mapping_parent @@ -20,7 +36,6 @@ CREATE TRIGGER trg_insert_active_tag_mapping_parent FOR EACH ROW EXECUTE FUNCTION insert_active_tag_mapping_parent(); - CREATE OR REPLACE FUNCTION delete_active_tag_mapping_parent() RETURNS TRIGGER AS $$ @@ -34,7 +49,7 @@ BEGIN RETURN old; END; -$$ LANGUAGE plpgsql; +$$ LANGUAGE plpgsql VOLATILE; -- Create the trigger for deletion CREATE TRIGGER trg_delete_active_tag_mapping_parent @@ -47,15 +62,24 @@ CREATE OR REPLACE FUNCTION insert_active_tag_mappings_parents_from_mappings() RETURNS TRIGGER AS $$ BEGIN - INSERT INTO active_tag_mappings_parents (record_id, tag_id, origin_id, tag_domain_id) - SELECT new.record_id, parent_id, new.tag_id, new.tag_domain_id + -- whenever a mapping is inserted into the active mappings + -- insert it's parents as well + + INSERT INTO active_tag_mappings_parents (record_id, tag_id, origin_id, tag_domain_id, internal, internal_count) + SELECT new.record_id, + parent_id, + new.tag_id, + new.tag_domain_id, + FALSE AS internal, + 0 AS internal_count FROM tag_parents tp WHERE tp.tag_domain_id = new.tag_domain_id - AND tp.child_id = new.tag_id; + AND tp.child_id = new.tag_id + ON CONFLICT (record_id, tag_id, origin_id, tag_domain_id) DO UPDATE SET internal = FALSE; RETURN new; END; -$$ LANGUAGE plpgsql; +$$ LANGUAGE plpgsql VOLATILE; CREATE TRIGGER trg_insert_active_tag_mappings_parents_from_mappings AFTER INSERT @@ -63,6 +87,39 @@ CREATE TRIGGER trg_insert_active_tag_mappings_parents_from_mappings FOR EACH ROW EXECUTE FUNCTION insert_active_tag_mappings_parents_from_mappings(); + +CREATE OR REPLACE FUNCTION delete_active_tag_mappings_parents_from_mappings() + RETURNS TRIGGER AS +$$ +BEGIN + + + UPDATE active_tag_mappings_parents + SET internal = TRUE + WHERE record_id = old.record_id + AND origin_id = old.tag_id + AND tag_domain_id = old.tag_domain_id + AND internal_count > 0; + + -- When a mapping is deleted from active_tag_mappings + -- remove its corresponding parent mappings + DELETE + FROM active_tag_mappings_parents + WHERE record_id = old.record_id + AND origin_id = old.tag_id + AND tag_domain_id = old.tag_domain_id + AND NOT internal; + + RETURN old; +END; +$$ LANGUAGE plpgsql VOLATILE; + +CREATE TRIGGER trg_delete_active_tag_mappings_parents_from_mappings + AFTER DELETE + ON active_tag_mappings + FOR EACH ROW +EXECUTE FUNCTION delete_active_tag_mappings_parents_from_mappings(); + INSERT INTO active_tag_mappings_parents (record_id, tag_id, origin_id, tag_domain_id) SELECT tm.record_id, tp.parent_id, tp.child_id, tp.tag_domain_id FROM active_tag_mappings tm @@ -80,7 +137,7 @@ BEGIN RETURN new; END; -$$ LANGUAGE plpgsql; +$$ LANGUAGE plpgsql VOLATILE; CREATE TRIGGER trg_intercept_active_tag_mappings_parents BEFORE INSERT diff --git a/IDHANMigration/src/93-tag_counts.sql b/IDHANMigration/src/93-tag_counts.sql index d2dd8ba..75b5ede 100644 --- a/IDHANMigration/src/93-tag_counts.sql +++ b/IDHANMigration/src/93-tag_counts.sql @@ -7,10 +7,53 @@ CREATE TABLE tag_counts PRIMARY KEY (tag_id, tag_domain_id) ); +CREATE TABLE total_tag_counts +( + tag_id BIGINT REFERENCES tags (tag_id), + storage_count INTEGER NOT NULL DEFAULT 0, + display_count INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (tag_id) +); + +CREATE OR REPLACE FUNCTION update_tag_counts(target_tag_id BIGINT) RETURNS VOID AS +$$ +BEGIN + -- Lock the tag_counts table to prevent concurrent updates + LOCK TABLE tag_counts IN EXCLUSIVE MODE; + + -- Update/Insert counts using a single UPSERT operation + INSERT INTO total_tag_counts (tag_id, storage_count, display_count) + SELECT target_tag_id, + COUNT(DISTINCT record_id) FILTER (WHERE atm.tag_id = target_tag_id) AS storage_count, + COUNT(DISTINCT record_id) FILTER (WHERE COALESCE(atm.ideal_tag_id, atm.tag_id) = target_tag_id) AS display_count + FROM active_tag_mappings atm + WHERE atm.tag_id = target_tag_id + OR COALESCE(atm.ideal_tag_id, atm.tag_id) = target_tag_id + ON CONFLICT (tag_id) DO UPDATE + SET storage_count = excluded.storage_count, + display_count = excluded.display_count; + + -- Update/Insert counts per domain using a single UPSERT operation + INSERT INTO tag_counts (tag_id, tag_domain_id, storage_count, display_count) + SELECT target_tag_id AS tag_id, + atm.tag_domain_id AS tag_domain_id, + COUNT(DISTINCT record_id) FILTER (WHERE atm.tag_id = target_tag_id) AS storage_count, + COUNT(DISTINCT record_id) FILTER (WHERE COALESCE(atm.ideal_tag_id, atm.tag_id) = target_tag_id) AS display_count + FROM active_tag_mappings atm + WHERE (atm.tag_id = target_tag_id OR COALESCE(atm.ideal_tag_id, atm.tag_id) = target_tag_id) + GROUP BY atm.tag_domain_id + ON CONFLICT (tag_id, tag_domain_id) DO UPDATE + SET storage_count = excluded.storage_count, + display_count = excluded.display_count; +END; +$$ LANGUAGE plpgsql; + CREATE FUNCTION add_count(tag_id_i BIGINT, ideal_tag_id_i BIGINT, tag_domain_id_i SMALLINT) RETURNS VOID AS $$ BEGIN + LOCK TABLE tag_counts IN EXCLUSIVE MODE; + IF tag_id_i IS NOT NULL THEN INSERT INTO tag_counts (tag_id, tag_domain_id, storage_count) VALUES (tag_id_i, tag_domain_id_i, 1) @@ -28,6 +71,9 @@ $$ LANGUAGE plpgsql; CREATE FUNCTION remove_count(tag_id_i BIGINT, ideal_tag_id_i BIGINT, tag_domain_id_i SMALLINT) RETURNS VOID AS $$ BEGIN + + LOCK TABLE tag_counts IN EXCLUSIVE MODE; + UPDATE tag_counts SET storage_count = storage_count - 1 WHERE tag_id = tag_id_i AND tag_domain_id = tag_domain_id_i; UPDATE tag_counts SET display_count = display_count - 1 WHERE tag_id = COALESCE(ideal_tag_id_i, tag_id_i) AND tag_domain_id = tag_domain_id_i; END; diff --git a/IDHANMigration/src/94-active_mappings_internal.sql b/IDHANMigration/src/94-active_mappings_internal.sql index 913671e..043b887 100644 --- a/IDHANMigration/src/94-active_mappings_internal.sql +++ b/IDHANMigration/src/94-active_mappings_internal.sql @@ -5,41 +5,25 @@ CREATE OR REPLACE FUNCTION atmp_internal_on_insert() RETURNS trigger AS $$ - -DECLARE - row RECORD; BEGIN - -- FOR new IN SELECT * FROM new_rows --- LOOP - -- Process each new row here - RAISE NOTICE 'Inserted: %', new; - - RAISE NOTICE 'Count: %', (SELECT COUNT(*) FROM tag_parents tp2); - - FOR row IN SELECT * FROM tag_parents tp2 - LOOP - RAISE NOTICE 'TP Row: %', row; - END LOOP; - --- INSERT INTO active_tag_mappings_parents (record_id, tag_id, origin_id, tag_domain_id) + INSERT INTO active_tag_mappings_parents (record_id, tag_id, origin_id, tag_domain_id, internal, internal_count) SELECT new.record_id AS record_id, COALESCE(tp.ideal_parent_id, tp.parent_id) AS tag_id, new.tag_id AS origin_id, - new.tag_domain_id AS tag_domain_id - INTO row + new.tag_domain_id AS tag_domain_id, + TRUE AS internal, + 1 AS internal_count FROM tag_parents tp WHERE COALESCE(tp.ideal_child_id, tp.child_id) = new.tag_id AND tp.tag_domain_id = new.tag_domain_id - LIMIT 1; - - RAISE NOTICE 'INSERT: %', row; - --- END LOOP; + ON CONFLICT (record_id, tag_id, origin_id, tag_domain_id) DO UPDATE SET internal_count = excluded.internal_count + 1; + -- if internal is true and there is an internal count, that means there are only internal tags + -- if internal is false but the count is non-zero, that means we have a direct parent RETURN new; END; -$$ LANGUAGE plpgsql; +$$ LANGUAGE plpgsql VOLATILE; -- ========================= -- Delete trigger function @@ -50,10 +34,18 @@ AS $$ BEGIN + UPDATE active_tag_mappings_parents + SET internal_count = internal_count - 1 + WHERE internal_count > 0 + AND record_id = old.record_id + AND origin_id = old.tag_id + AND tag_domain_id = old.tag_domain_id; + + DELETE FROM active_tag_mappings_parents WHERE internal AND internal_count = 0; RETURN new; END; -$$ LANGUAGE plpgsql; +$$ LANGUAGE plpgsql VOLATILE; -- ========================= -- Triggers diff --git a/IDHANMigration/src/management.cpp b/IDHANMigration/src/management.cpp index 269f4b1..b60b36c 100644 --- a/IDHANMigration/src/management.cpp +++ b/IDHANMigration/src/management.cpp @@ -59,26 +59,4 @@ void addTableToInfo( params ); } -#ifdef ALLOW_TABLE_DESTRUCTION - -void destroyTables( pqxx::nontransaction& tx ) -{ - // log::critical( - // "We are about to drop the public schema since we are compiling with ALLOW_TABLE_DESTRUCTION! This will happen in 5 seconds. QUIT NOW IF YOU DON'T WANT THIS TO HAPPEN" ); - // std::this_thread::sleep_for( std::chrono::seconds( 5 ) ); - - if ( !tx.exec( "SELECT schema_name FROM information_schema.schemata WHERE schema_name = \'public\'" ).empty() ) - { - tx.exec( "DROP SCHEMA public CASCADE" ); - } - else - { - spdlog::debug( "Public schema does not exist. Skipping drop." ); - } - - tx.exec( "CREATE SCHEMA public" ); -} - -#endif - } // namespace idhan::db diff --git a/IDHANServer/CMakeLists.txt b/IDHANServer/CMakeLists.txt index 979d276..0f662e6 100644 --- a/IDHANServer/CMakeLists.txt +++ b/IDHANServer/CMakeLists.txt @@ -3,7 +3,6 @@ project(IDHANServer LANGUAGES CXX C) AddFGLExecutable(IDHANServer ${CMAKE_CURRENT_SOURCE_DIR}/src) - target_sources(IDHANServer PRIVATE ${MIGRATION_SOURCE}) # Gui is needed for QImage for whatever reason @@ -22,10 +21,6 @@ target_link_libraries(IDHANServer PRIVATE IDHAN) target_link_libraries(IDHANServer PUBLIC IDHANModules IDHANMigration) target_compile_definitions(IDHANServer PUBLIC IDHAN_USE_STD_FORMAT) -if (DEFINED ALLOW_TABLE_DESTRUCTION AND ALLOW_TABLE_DESTRUCTION) - target_compile_definitions(IDHANServer PUBLIC ALLOW_TABLE_DESTRUCTION=1) -endif () - # Copy page info to the output directory file(GLOB_RECURSE PAGE_FILES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/static/*") diff --git a/IDHANServer/src/api/tags/autocompleteTag.cpp b/IDHANServer/src/api/tags/autocompleteTag.cpp index 9a7c047..661f0c2 100644 --- a/IDHANServer/src/api/tags/autocompleteTag.cpp +++ b/IDHANServer/src/api/tags/autocompleteTag.cpp @@ -16,21 +16,28 @@ drogon::Task< Json::Value > const auto wrapped_search_value { '%' + search_value + '%' }; + constexpr std::size_t max_limit { 32 }; + + if ( limit > max_limit ) + { + log::warn( "Tag search came in with absurdly high limit (was {}, clamped to {})", limit, max_limit ); + } + const auto result { co_await db->execSqlCoro( R"( SELECT *, similarity(tag_text, $2) AS similarity, tag_text = $2 AS exact, similarity(tag_text, $2) * coalesce(display_count, 1) AS score, - tag_counts.display_count AS count + COALESCE(tc.display_count, 0) AS count FROM tags_combined - JOIN tag_counts USING (tag_id) + LEFT JOIN total_tag_counts tc USING (tag_id) WHERE tag_text LIKE $1 ORDER BY exact DESC, score DESC, similarity DESC limit $3)", wrapped_search_value, search_value, - limit ) }; + std::min( limit, max_limit ) ) }; Json::Value root {}; diff --git a/IDHANServer/src/db/ManagementConnection.cpp b/IDHANServer/src/db/ManagementConnection.cpp index d6e9304..f915906 100644 --- a/IDHANServer/src/db/ManagementConnection.cpp +++ b/IDHANServer/src/db/ManagementConnection.cpp @@ -12,13 +12,6 @@ namespace idhan { -void ManagementConnection::initalSetup( pqxx::nontransaction& tx ) -{ - log::info( "Starting inital table setup" ); - - tx.commit(); -} - ManagementConnection::ManagementConnection( const ConnectionArguments& arguments ) : connection( arguments.format() ) { log::info( "Postgres connection made: {}", connection.dbname() ); @@ -37,7 +30,6 @@ ManagementConnection::ManagementConnection( const ConnectionArguments& arguments { tx.exec( "CREATE SCHEMA IF NOT EXISTS public" ); constexpr std::string_view schema { "public" }; - db::destroyTables( tx ); db::updateMigrations( tx, schema ); } diff --git a/IDHANServer/src/db/ManagementConnection.hpp b/IDHANServer/src/db/ManagementConnection.hpp index a384a74..ce4d8f2 100644 --- a/IDHANServer/src/db/ManagementConnection.hpp +++ b/IDHANServer/src/db/ManagementConnection.hpp @@ -20,8 +20,6 @@ class ManagementConnection { pqxx::connection connection; - void initalSetup( pqxx::nontransaction& nontransaction ); - public: inline pqxx::connection& conn() { return connection; } diff --git a/tests/src/server/tags/MappingFixture.cpp b/tests/src/server/tags/MappingFixture.cpp index 33da857..2b92e25 100644 --- a/tests/src/server/tags/MappingFixture.cpp +++ b/tests/src/server/tags/MappingFixture.cpp @@ -4,29 +4,29 @@ #include "MappingFixture.hpp" -void MappingFixture::createMapping( TagID tag_id ) +void MappingFixture::createMapping( TagID tag_id, const RecordID record_id ) { pqxx::work tx { *conn }; - tx.exec_params( "INSERT INTO tag_mappings (tag_id, record_id, tag_domain_id) VALUES ($1, $2, $3)", pqxx::params { tag_id, default_record_id, default_domain_id } ); + tx.exec_params( "INSERT INTO tag_mappings (tag_id, record_id, tag_domain_id) VALUES ($1, $2, $3)", pqxx::params { tag_id, record_id, default_domain_id } ); tx.commit(); } -void MappingFixture::deleteMapping( TagID tag_id ) +void MappingFixture::deleteMapping( TagID tag_id, const RecordID record_id ) { pqxx::work tx { *conn }; - tx.exec_params( "DELETE FROM tag_mappings WHERE tag_id = $1 AND record_id = $2 AND tag_domain_id = $3", pqxx::params { tag_id, default_record_id, default_domain_id } ); + tx.exec_params( "DELETE FROM tag_mappings WHERE tag_id = $1 AND record_id = $2 AND tag_domain_id = $3", pqxx::params { tag_id, record_id, default_domain_id } ); tx.commit(); } -bool MappingFixture::mappingExists( TagID tag_id ) +bool MappingFixture::mappingExists( TagID tag_id, const RecordID record_id ) { pqxx::work tx { *conn }; - const auto result { tx.exec_params( "SELECT EXISTS(SELECT 1 FROM tag_mappings WHERE tag_id = $1 AND record_id = $2 AND tag_domain_id = $3)", pqxx::params { tag_id, default_record_id, default_domain_id } ) }; + const auto result { tx.exec_params( "SELECT EXISTS(SELECT 1 FROM tag_mappings WHERE tag_id = $1 AND record_id = $2 AND tag_domain_id = $3)", pqxx::params { tag_id, record_id, default_domain_id } ) }; tx.commit(); @@ -39,16 +39,16 @@ RecordID MappingFixture::createRecord( const std::string_view data ) const auto result { tx.exec_params( "INSERT INTO records (sha256) VALUES (digest($1, 'sha256')) RETURNING record_id", pqxx::params { data } ) }; - tx.commit(); - if ( result.empty() ) throw std::runtime_error( "Failed to create record" ); + tx.exec_params( "INSERT INTO file_info (size, record_id) VALUES ($1, $2)", 1, result[ 0 ][ 0 ].as< RecordID >() ); + + tx.commit(); + return result[ 0 ][ 0 ].as< RecordID >(); } void MappingFixture::SetUp() { ServerTagFixture::SetUp(); - - default_record_id = createRecord( "test" ); } diff --git a/tests/src/server/tags/MappingFixture.hpp b/tests/src/server/tags/MappingFixture.hpp index 474ce18..401e5c2 100644 --- a/tests/src/server/tags/MappingFixture.hpp +++ b/tests/src/server/tags/MappingFixture.hpp @@ -10,9 +10,9 @@ class MappingFixture : public ServerTagFixture protected: - void createMapping( TagID tag_id ); - void deleteMapping( TagID tag_id ); - bool mappingExists( TagID tag_id ); + void createMapping( TagID tag_id, RecordID record_id ); + void deleteMapping( TagID tag_id, RecordID record_id ); + bool mappingExists( TagID tag_id, RecordID record_id ); RecordID createRecord( const std::string_view data ); void SetUp() override; diff --git a/tests/src/server/tags/ServerTagFixture.cpp b/tests/src/server/tags/ServerTagFixture.cpp index ec8bd14..944ee38 100644 --- a/tests/src/server/tags/ServerTagFixture.cpp +++ b/tests/src/server/tags/ServerTagFixture.cpp @@ -4,7 +4,9 @@ #include "ServerTagFixture.hpp" +#include "MappingFixture.hpp" #include "ServerDBFixture.hpp" +#include "logging/format_ns.hpp" #include "migrations.hpp" #include "splitTag.hpp" @@ -73,4 +75,129 @@ bool ServerTagFixture::aliasExists( const TagID aliased_id, const TagID alias_id tx.commit(); return result[ 0 ][ 0 ].as< bool >(); +} + +void ServerTagFixture::createParent( TagID parent_id, TagID child_id ) +{ + if ( !conn ) throw std::runtime_error( "Connection was nullptr" ); + pqxx::work tx { *conn }; + + tx.exec_params( "INSERT INTO tag_parents (parent_id, child_id, tag_domain_id) VALUES ($1, $2, $3)", pqxx::params { parent_id, child_id, default_domain_id } ); + + tx.commit(); + + return; +} + +bool ServerTagFixture::parentExists( TagID parent_id, TagID child_id ) +{ + if ( !conn ) throw std::runtime_error( "Connection was nullptr" ); + pqxx::work tx { *conn }; + + const auto result { tx.exec_params( "SELECT EXISTS(SELECT 1 FROM tag_parents WHERE parent_id = $1 AND child_id = $2 AND tag_domain_id = $3)", pqxx::params { parent_id, child_id, default_domain_id } ) }; + + return result[ 0 ][ 0 ].as< bool >(); +} + +testing::AssertionResult dumpParents( testing::AssertionResult result, pqxx::work& tx ) +{ + const auto table_printout { tx.exec( "SELECT * FROM tag_parents" ) }; + + result << "Parents (tag_parents):\n"; + result << "(parent_id, child_id, ideal_parent_id, ideal_child_id)\n"; + for ( const auto& row : table_printout ) + { + const auto parent_id { row[ "parent_id" ].as< TagID >() }; + const auto child_id { row[ "child_id" ].as< TagID >() }; + const auto is_parent_ideal { !row[ "ideal_parent_id" ].is_null() }; + const auto is_child_ideal { !row[ "ideal_child_id" ].is_null() }; + + if ( is_parent_ideal && is_child_ideal ) + { + result << format_ns::format( "({}, {}, {}, {})\n", parent_id, child_id, row[ "ideal_parent_id" ].as< TagID >(), row[ "ideal_child_id" ].as< TagID >() ); + } + else if ( is_parent_ideal ) + { + result << format_ns::format( "({}, {}, {}, NULL)\n", parent_id, child_id, row[ "ideal_parent_id" ].as< TagID >() ); + } + else if ( is_child_ideal ) + { + result << format_ns::format( "({}, {}, NULL, {})\n", parent_id, child_id, row[ "ideal_child_id" ].as< TagID >() ); + } + else + { + result << format_ns::format( "({}, {}, NULL, NULL)\n", parent_id, child_id ); + } + } + + return result; +} + +testing::AssertionResult dumpMappings( testing::AssertionResult result, pqxx::work& tx ) +{ + const auto table_printout { tx.exec( "SELECT * FROM active_tag_mappings" ) }; + + result << "Mappings (active_tag_mappings):\n"; + result << "(record_id, tag_id, tag_domain_id, ideal_tag_id)\n"; + for ( const auto& row : table_printout ) + { + const auto record_id { row[ "record_id" ].as< RecordID >() }; + const auto tag_id { row[ "tag_id" ].as< TagID >() }; + const auto tag_domain_id { row[ "tag_domain_id" ].as< TagDomainID >() }; + + const bool is_ideal { !row[ "ideal_tag_id" ].is_null() }; + + if ( is_ideal ) + result << format_ns::format( "({}, {}, {}, {})\n", record_id, tag_id, tag_domain_id, row[ "ideal_tag_id" ].as< TagID >() ); + else + result << format_ns::format( "({}, {}, {}, NULL)\n", record_id, tag_id, tag_domain_id ); + } + + if ( table_printout.size() == 0 ) + result << "\t\t\tNo rows found\n"; + + return result; +} + +testing::AssertionResult dumpParentMappings( testing::AssertionResult result, pqxx::work& tx ) +{ + result = dumpMappings( result, tx ); + result = dumpParents( result, tx ); + + const auto table_printout { tx.exec( "SELECT * FROM active_tag_mappings_parents" ) }; + + result << "Parent mappings (active_tag_mappings_parents):\n"; + result << "(record_id, tag_id, origin_id, tag_domain_id, internal, internal_count)\n"; + + for ( const auto& row : table_printout ) + { + const auto record_id { row[ "record_id" ].as< RecordID >() }; + const auto tag_id { row[ "tag_id" ].as< TagID >() }; + const auto origin_id { row[ "origin_id" ].as< TagID >() }; + const auto tag_domain_id { row[ "tag_domain_id" ].as< TagDomainID >() }; + const bool internal { row[ "internal" ].as< bool >() }; + const auto internal_count { row[ "internal_count" ].as< std::uint32_t >() }; + + result << format_ns::format( "({}, {}, {}, {}, {}, {})\n", record_id, tag_id, origin_id, tag_domain_id, internal, internal_count ); + } + + if ( table_printout.size() == 0 ) + result << "\t\t\tNo rows found\n"; + + return result; +} + +testing::AssertionResult ServerTagFixture::parentInternalExists( const RecordID record_id, TagID parent_id, TagID child_id, std::uint32_t count ) +{ + if ( !conn ) throw std::runtime_error( "Connection was nullptr" ); + pqxx::work tx { *conn }; + + const auto result { tx.exec_params( "SELECT EXISTS(SELECT 1 FROM active_tag_mappings_parents WHERE record_id = $5 AND tag_id = $1 AND origin_id = $2 AND tag_domain_id = $3 AND internal_count = $4)", pqxx::params { parent_id, child_id, default_domain_id, count, record_id } ) }; + + if ( result[ 0 ][ 0 ].as< bool >() == false ) + { + return dumpParentMappings( testing::AssertionFailure(), tx ); + } + + return testing::AssertionSuccess(); } \ No newline at end of file diff --git a/tests/src/server/tags/ServerTagFixture.hpp b/tests/src/server/tags/ServerTagFixture.hpp index 09c5a1c..d6ac20f 100644 --- a/tests/src/server/tags/ServerTagFixture.hpp +++ b/tests/src/server/tags/ServerTagFixture.hpp @@ -25,5 +25,11 @@ class ServerTagFixture : public ServerDBFixture bool aliasExists( TagID aliased_id, TagID alias_id ); + void createParent( TagID parent_id, TagID child_id ); + + bool parentExists( TagID parent_id, TagID child_id ); + + testing::AssertionResult parentInternalExists( RecordID record_id, TagID parent_id, TagID child_id, std::uint32_t count = 1 ); + TagDomainID default_domain_id { 0 }; }; diff --git a/tests/src/server/tags/mappings.cpp b/tests/src/server/tags/mappings.cpp index e2b5240..28681f1 100644 --- a/tests/src/server/tags/mappings.cpp +++ b/tests/src/server/tags/mappings.cpp @@ -9,11 +9,13 @@ TEST_F( MappingFixture, StorageMapping ) const auto tag_1 { createTag( "tag:1" ) }; const auto tag_2 { createTag( "tag:2" ) }; - createMapping( tag_1 ); - createMapping( tag_2 ); + const auto record { createRecord( "record" ) }; - ASSERT_TRUE( mappingExists( tag_1 ) ); - ASSERT_TRUE( mappingExists( tag_2 ) ); + createMapping( tag_1, record ); + createMapping( tag_2, record ); + + ASSERT_TRUE( mappingExists( tag_1, record ) ); + ASSERT_TRUE( mappingExists( tag_2, record ) ); SUCCEED(); } \ No newline at end of file diff --git a/tests/src/server/tags/parents.cpp b/tests/src/server/tags/parents.cpp new file mode 100644 index 0000000..02fdaef --- /dev/null +++ b/tests/src/server/tags/parents.cpp @@ -0,0 +1,35 @@ + +#include "MappingFixture.hpp" +#include "ServerTagFixture.hpp" + +TEST_F( MappingFixture, TagParentCreation ) +{ + const auto tag_ahri { createTag( "ahri (league of legends)" ) }; + const auto tag_kogmaw { createTag( "kogmaw (league of legends)" ) }; + + const auto tag_league { createTag( "series:league of legends" ) }; + const auto tag_riot_games { createTag( "copyright:riot games" ) }; + + // test adding to existing + const auto record_1 { createRecord( "record_1" ) }; + + createMapping( tag_ahri, record_1 ); + createMapping( tag_kogmaw, record_1 ); + + createParent( tag_league, tag_ahri ); + createParent( tag_league, tag_kogmaw ); + + createParent( tag_riot_games, tag_league ); + + ASSERT_TRUE( parentExists( tag_league, tag_ahri ) ); + ASSERT_TRUE( parentExists( tag_league, tag_kogmaw ) ); + + // test adding to new mappings + const auto record_2 { createRecord( "record_2" ) }; + + createMapping( tag_ahri, record_2 ); + createMapping( tag_kogmaw, record_2 ); + + ASSERT_TRUE( parentInternalExists( record_1, tag_riot_games, tag_league, 2 ) ); + ASSERT_TRUE( parentInternalExists( record_2, tag_riot_games, tag_league, 2 ) ); +}