diff --git a/include/orm/tiny/model.hpp b/include/orm/tiny/model.hpp index 17c5f0d35..1353e9930 100644 --- a/include/orm/tiny/model.hpp +++ b/include/orm/tiny/model.hpp @@ -944,33 +944,9 @@ namespace Orm::Tiny Derived Model::replicate( const std::unordered_set &except) { - std::unordered_set defaults { - getKeyName(), - this->getCreatedAtColumn(), - this->getUpdatedAtColumn(), - }; - // Remove empty attribute names - std::erase_if(defaults, [](const auto &attribute) - { - return attribute.isEmpty(); - }); - - // Merge defaults into except - std::unordered_set exceptMerged(defaults.size() + except.size()); - if (!except.empty()) { - exceptMerged = except; - exceptMerged.merge(defaults); - } - else - exceptMerged = std::move(defaults); - - // Get all attributes excluding those in the exceptMerged set - auto attributes = this->getAttributes() - | ranges::views::filter([&exceptMerged](const AttributeItem &attribute) - { - return !exceptMerged.contains(attribute.key); - }) - | ranges::to>(); + /* Get all attributes excluding the primary key, created_at, and updated_at + attributes and those in the except set. */ + auto attributes = AttributeUtils::exceptAttributesForReplicate(*this, except); /* Create a new instance (with correctly set a table and connection names), set obtained attributes and relations. */ diff --git a/include/orm/tiny/relations/concerns/interactswithpivottable.hpp b/include/orm/tiny/relations/concerns/interactswithpivottable.hpp index a487f2d0c..66874cf6c 100644 --- a/include/orm/tiny/relations/concerns/interactswithpivottable.hpp +++ b/include/orm/tiny/relations/concerns/interactswithpivottable.hpp @@ -9,7 +9,6 @@ TINY_SYSTEM_HEADER #include #include -#include #include #include "orm/exceptions/domainerror.hpp" diff --git a/include/orm/tiny/utils/attribute.hpp b/include/orm/tiny/utils/attribute.hpp index 8f5033584..9e7cfc0e9 100644 --- a/include/orm/tiny/utils/attribute.hpp +++ b/include/orm/tiny/utils/attribute.hpp @@ -5,6 +5,11 @@ #include "orm/macros/systemheader.hpp" TINY_SYSTEM_HEADER +#include + +#include +#include + #include "orm/tiny/tinytypes.hpp" TINYORM_BEGIN_COMMON_NAMESPACE @@ -49,8 +54,51 @@ namespace Orm::Tiny::Utils joinAttributesForFirstOr(const QVector &attributes, const QVector &values, const QString &keyName); + + /*! Remove a given attributes from the model attributes vector and return + a copy. */ + template + static QVector + exceptAttributesForReplicate(const Model &model, + const std::unordered_set &except = {}); }; + /* public */ + + template + QVector + Attribute::exceptAttributesForReplicate(const Model &model, + const std::unordered_set &except) + { + std::unordered_set defaults { + model.getKeyName(), + model.getCreatedAtColumn(), + model.getUpdatedAtColumn(), + }; + // Remove empty attribute names + std::erase_if(defaults, [](const auto &attribute) + { + return attribute.isEmpty(); + }); + + // Merge defaults into except + std::unordered_set exceptMerged(defaults.size() + except.size()); + if (!except.empty()) { + exceptMerged = except; + exceptMerged.merge(defaults); + } + else + exceptMerged = std::move(defaults); + + // Get all attributes excluding those in the exceptMerged set + return model.getAttributes() + | ranges::views::filter([&exceptMerged](const AttributeItem &attribute) + { + return !exceptMerged.contains(attribute.key); + }) + | ranges::to>(); + } + } // namespace Orm::Tiny::Utils TINYORM_END_COMMON_NAMESPACE diff --git a/src/orm/tiny/utils/attribute.cpp b/src/orm/tiny/utils/attribute.cpp index e5c627a01..375a1a5f9 100644 --- a/src/orm/tiny/utils/attribute.cpp +++ b/src/orm/tiny/utils/attribute.cpp @@ -3,8 +3,6 @@ #include #include -#include -#include #include TINYORM_BEGIN_COMMON_NAMESPACE diff --git a/tests/auto/functional/orm/tiny/model_conn_indep/tst_model_connection_independent.cpp b/tests/auto/functional/orm/tiny/model_conn_indep/tst_model_connection_independent.cpp index c01a94c4d..b3ab581fd 100644 --- a/tests/auto/functional/orm/tiny/model_conn_indep/tst_model_connection_independent.cpp +++ b/tests/auto/functional/orm/tiny/model_conn_indep/tst_model_connection_independent.cpp @@ -21,10 +21,12 @@ using Orm::Constants::SIZE; using Orm::DB; using Orm::Exceptions::MultipleRecordsFoundError; using Orm::Exceptions::RecordsNotFoundError; +using Orm::One; using Orm::Tiny::ConnectionOverride; using Orm::Tiny::Exceptions::MassAssignmentError; +using AttributeUtils = Orm::Tiny::Utils::Attribute; using TypeUtils = Orm::Utils::Type; using TestUtils::Databases; @@ -36,6 +38,8 @@ using Models::Torrent_AllowedMassAssignment; using Models::Torrent_GuardedAttribute; using Models::Torrent_TotallyGuarded; using Models::TorrentEager; +using Models::TorrentPreviewableFileProperty; +using Models::User; class tst_Model_Connection_Independent : public QObject // clazy:exclude=ctor-missing-parent-argument { @@ -56,6 +60,10 @@ private slots: void equalComparison() const; void notEqualComparison() const; + void replicate() const; + void replicate_WithCreate() const; + void replicate_WithRelations() const; + void defaultAttributeValues() const; void massAssignment_Fillable() const; @@ -359,6 +367,119 @@ void tst_Model_Connection_Independent::notEqualComparison() const } } +void tst_Model_Connection_Independent::replicate() const +{ + auto user1 = User::find(1); + QVERIFY(user1); + QVERIFY(user1->exists); + + auto user1Replicated = user1->replicate(); + + auto user1Attributes = AttributeUtils::exceptAttributesForReplicate(*user1); + auto user1ReplicatedAttributes = AttributeUtils::exceptAttributesForReplicate( + user1Replicated); + + QVERIFY(!user1Replicated.exists); + QVERIFY(user1Attributes == user1ReplicatedAttributes); + QVERIFY(user1->getRelations().empty()); + QVERIFY(user1Replicated.getRelations().empty()); +} + +void tst_Model_Connection_Independent::replicate_WithCreate() const +{ + // Following is the most used case for the replicate method so I will test it + auto user = User::create({{"name", "xyz"}, + {"is_banned", true}, + {"note", "test"}}); + QVERIFY(user.exists); + + std::unordered_set except {NAME}; + + auto userReplicated = user.replicate(except); + + auto userAttributes = AttributeUtils::exceptAttributesForReplicate(user, except); + auto userReplicatedAttributes = AttributeUtils::exceptAttributesForReplicate( + userReplicated); + + QVERIFY(!userReplicated.exists); + QVERIFY(userAttributes == userReplicatedAttributes); + + QCOMPARE(user.getAttribute(NAME), QVariant("xyz")); + QVERIFY(!userReplicated.getAttribute(NAME).isValid()); + + QVERIFY(user.getRelations().empty()); + QVERIFY(userReplicated.getRelations().empty()); + + // Restore db + QVERIFY(user.remove()); +} + +void tst_Model_Connection_Independent::replicate_WithRelations() const +{ + auto torrent2 = Torrent::with("torrentFiles.fileProperty")->find(2); + QVERIFY(torrent2); + QVERIFY(torrent2->exists); + + auto torrent2Replicated = torrent2->replicate(); + + auto torrent2Attributes = AttributeUtils::exceptAttributesForReplicate(*torrent2); + auto torrent2ReplicatedAttributes = AttributeUtils::exceptAttributesForReplicate( + torrent2Replicated); + + const auto &torrent2Relations = torrent2->getRelations(); + const auto &torrent2ReplicatedRelations = torrent2Replicated.getRelations(); + + /* Crazy, but I'm going to check all relations 😮😎, yeah it's definitely crazy and + overmotivated, but it checks almost everything I wanted. 🙃🤙 */ + // Torrent + QVERIFY(!torrent2Replicated.exists); + QVERIFY(torrent2Attributes == torrent2ReplicatedAttributes); + QVERIFY(torrent2Relations.size() == torrent2ReplicatedRelations.size()); + QVERIFY(torrent2->relationLoaded("torrentFiles")); + QVERIFY(torrent2Replicated.relationLoaded("torrentFiles")); + // The Model::operator== was needed to make this real + QVERIFY(torrent2Relations == torrent2ReplicatedRelations); + + // TorrentPreviewableFile + const auto torrentFiles2 = + torrent2->getRelationValue("torrentFiles"); + const auto torrentFiles2Replicated = + torrent2Replicated.getRelationValue("torrentFiles"); + + QVERIFY(torrentFiles2.size() == torrentFiles2Replicated.size()); + + for (std::remove_cvref_t::size_type i = 0; + torrentFiles2.size() < i; ++i + ) { + auto *torrentFile = torrentFiles2.value(i); + QVERIFY(torrentFile->exists); + QVERIFY(torrentFile->relationLoaded("fileProperty")); + + auto *torrentFileReplicated = torrentFiles2Replicated.value(i); + QVERIFY(torrentFileReplicated->exists); + QVERIFY(torrentFileReplicated->relationLoaded("fileProperty")); + + QVERIFY(torrentFile->getAttributes() == torrentFileReplicated->getAttributes()); + // The Model::operator== was needed to make this real + QVERIFY(torrentFile->getRelations() == torrentFileReplicated->getRelations()); + + // TorrentPreviewableFileProperty + auto *fileProperty = + torrentFile->getRelationValue( + "fileProperty"); + auto *filePropertyReplicated = + torrentFileReplicated->getRelationValue("fileProperty"); + + QVERIFY(fileProperty->exists); + QVERIFY(fileProperty->getRelations().empty()); + QVERIFY(filePropertyReplicated->exists); + QVERIFY(filePropertyReplicated->getRelations().empty()); + QVERIFY(fileProperty->getAttributes() == + filePropertyReplicated->getAttributes()); + } +} + void tst_Model_Connection_Independent::defaultAttributeValues() const { {