diff --git a/src/Lightweight/DataMapper/DataMapper.hpp b/src/Lightweight/DataMapper/DataMapper.hpp index c52a584e..705d7886 100644 --- a/src/Lightweight/DataMapper/DataMapper.hpp +++ b/src/Lightweight/DataMapper/DataMapper.hpp @@ -350,9 +350,17 @@ class DataMapper template bool IsModified(Record const& record) const noexcept; - /// Clears the modified state of the record. - template - void ClearModifiedState(Record& record) noexcept; + /// Enum to set the modified state of a record. + enum class ModifiedState : uint8_t + { + Modified, + NotModified + }; + + /// Sets the modified state of the record after receiving from the database. + /// This marks all fields as not modified. + template + void SetModifiedState(Record& record) noexcept; /// Loads all direct relations to this record. template @@ -1323,15 +1331,15 @@ RecordPrimaryKeyType DataMapper::Create(Record& record) if constexpr (FieldType::IsPrimaryKey) if constexpr (FieldType::IsAutoAssignPrimaryKey) { - if (!record.[:el:].IsModified()) + using ValueType = typename FieldType::ValueType; + if constexpr (std::same_as) { - using ValueType = typename FieldType::ValueType; - if constexpr (std::same_as) - { - if (!record.[:el:].Value()) - record.[:el:] = SqlGuid::Create(); - } - else if constexpr (requires { ValueType {} + 1; }) + if (!record.[:el:].Value()) + record.[:el:] = SqlGuid::Create(); + } + else if constexpr (requires { ValueType {} + 1; }) + { + if (record.[:el:].Value() == ValueType {}) { auto maxId = SqlStatement { _connection }.ExecuteDirectScalar(std::format( R"sql(SELECT MAX("{}") FROM "{}")sql", FieldNameOf, RecordTableName)); @@ -1344,15 +1352,15 @@ RecordPrimaryKeyType DataMapper::Create(Record& record) CallOnPrimaryKey(record, [&](PrimaryKeyType& primaryKeyField) { if constexpr (PrimaryKeyType::IsAutoAssignPrimaryKey) { - if (!primaryKeyField.IsModified()) + using ValueType = PrimaryKeyType::ValueType; + if constexpr (std::same_as) { - using ValueType = PrimaryKeyType::ValueType; - if constexpr (std::same_as) - { - if (!primaryKeyField.Value()) - primaryKeyField = SqlGuid::Create(); - } - else if constexpr (requires { ValueType {} + 1; }) + if (!primaryKeyField.Value()) + primaryKeyField = SqlGuid::Create(); + } + else if constexpr (requires { ValueType {} + 1; }) + { + if (primaryKeyField.Value() == ValueType {}) { auto maxId = SqlStatement { _connection }.ExecuteDirectScalar( std::format(R"sql(SELECT MAX("{}") FROM "{}")sql", @@ -1370,7 +1378,7 @@ RecordPrimaryKeyType DataMapper::Create(Record& record) if constexpr (HasAutoIncrementPrimaryKey) SetId(record, _stmt.LastInsertId(RecordTableName)); - ClearModifiedState(record); + SetModifiedState(record); if constexpr (QueryOptions.loadRelations) ConfigureRelationAutoLoading(record); @@ -1474,7 +1482,7 @@ void DataMapper::Update(Record& record) _stmt.Execute(); - ClearModifiedState(record); + SetModifiedState(record); } template @@ -1550,6 +1558,9 @@ std::optional DataMapper::QuerySingleWithoutRelationAutoLoading(PrimaryK if (!detail::ReadSingleResult(_stmt.Connection().ServerType(), reader, *resultRecord)) return std::nullopt; + if (resultRecord) + SetModifiedState(resultRecord.value()); + return resultRecord; } @@ -1558,7 +1569,9 @@ std::optional DataMapper::QuerySingle(PrimaryKeyTypes&&... primaryKeys) { auto record = QuerySingleWithoutRelationAutoLoading(std::forward(primaryKeys)...); if (record) + { ConfigureRelationAutoLoading(*record); + } return record; } @@ -1578,6 +1591,10 @@ std::optional DataMapper::QuerySingle(SqlSelectQueryBuilder selectQuery, auto reader = _stmt.GetResultCursor(); if (!detail::ReadSingleResult(_stmt.Connection().ServerType(), reader, *resultRecord)) return std::nullopt; + + if (resultRecord) + SetModifiedState(resultRecord.value()); + return resultRecord; } @@ -1638,7 +1655,10 @@ std::vector DataMapper::Query(std::string_view sqlQueryString, InputPara result.pop_back(); for (auto& record: result) + { + SetModifiedState(record); ConfigureRelationAutoLoading(record); + } } return result; @@ -1710,16 +1730,16 @@ std::vector> DataMapper::Query(SqlSelectQuery // Drop the last record, which we failed to fetch (End of result set). result.pop_back(); - if constexpr (QueryOptions.loadRelations) + for (auto& record: result) { - - for (auto& record: result) - { - Reflection::template_for<0, std::tuple_size_v>([&]() { - auto& element = std::get(record); + Reflection::template_for<0, std::tuple_size_v>([&]() { + auto& element = std::get(record); + SetModifiedState(element); + if constexpr (QueryOptions.loadRelations) + { ConfigureRelationAutoLoading(element); - }); - } + } + }); } return result; @@ -1759,13 +1779,16 @@ std::vector DataMapper::Query(SqlSelectQueryBuilder::ComposedQuery const records.pop_back(); for (auto& record: records) + { + SetModifiedState(record); ConfigureRelationAutoLoading(record); + } return records; } -template -void DataMapper::ClearModifiedState(Record& record) noexcept +template +void DataMapper::SetModifiedState(Record& record) noexcept { static_assert(!std::is_const_v); static_assert(DataMapperRecord, "Record must satisfy DataMapperRecord"); @@ -1773,7 +1796,10 @@ void DataMapper::ClearModifiedState(Record& record) noexcept Reflection::EnumerateMembers(record, [](FieldType& field) { if constexpr (requires { field.SetModified(false); }) { - field.SetModified(false); + if constexpr (state == ModifiedState::Modified) + field.SetModified(true); + else + field.SetModified(false); } }); } diff --git a/src/Lightweight/DataMapper/Field.hpp b/src/Lightweight/DataMapper/Field.hpp index 75da351b..d6037b10 100644 --- a/src/Lightweight/DataMapper/Field.hpp +++ b/src/Lightweight/DataMapper/Field.hpp @@ -126,20 +126,20 @@ struct Field constexpr std::weak_ordering operator<=>(Field const& other) const noexcept; /// Compares the field value with the given value for equality. - bool operator==(Field const& value) const noexcept = default; + constexpr bool operator==(Field const& value) const noexcept; /// Compares the field value with the given value for inequality. - bool operator!=(Field const& value) const noexcept = default; + constexpr bool operator!=(Field const& value) const noexcept; /// Compares the field value with the given value for equality. template requires std::convertible_to - bool operator==(S const& value) const noexcept; + constexpr bool operator==(S const& value) const noexcept; /// Compares the field value with the given value for inequality. template requires std::convertible_to - bool operator!=(S const& value) const noexcept; + constexpr bool operator!=(S const& value) const noexcept; /// Returns a string representation of the value, suitable for use in debugging and logging. [[nodiscard]] std::string InspectValue() const; @@ -167,7 +167,7 @@ struct Field private: ValueType _value {}; - bool _modified { false }; + bool _modified { true }; }; // clang-format off @@ -237,10 +237,22 @@ constexpr std::weak_ordering LIGHTWEIGHT_FORCE_INLINE Field::operator return _value <=> other._value; } +template +constexpr bool LIGHTWEIGHT_FORCE_INLINE Field::operator==(Field const& other) const noexcept +{ + return _value == other._value; +} + +template +constexpr bool LIGHTWEIGHT_FORCE_INLINE Field::operator!=(Field const& other) const noexcept +{ + return _value != other._value; +} + template template requires std::convertible_to -inline LIGHTWEIGHT_FORCE_INLINE bool Field::operator==(S const& value) const noexcept +constexpr bool LIGHTWEIGHT_FORCE_INLINE Field::operator==(S const& value) const noexcept { return _value == value; } @@ -248,7 +260,7 @@ inline LIGHTWEIGHT_FORCE_INLINE bool Field::operator==(S const& value template template requires std::convertible_to -inline LIGHTWEIGHT_FORCE_INLINE bool Field::operator!=(S const& value) const noexcept +constexpr bool LIGHTWEIGHT_FORCE_INLINE Field::operator!=(S const& value) const noexcept { return _value != value; } diff --git a/src/tests/DataMapper/CreateTests.cpp b/src/tests/DataMapper/CreateTests.cpp index 13d1c1d5..14ccb0e5 100644 --- a/src/tests/DataMapper/CreateTests.cpp +++ b/src/tests/DataMapper/CreateTests.cpp @@ -136,6 +136,7 @@ TEST_CASE_METHOD(SqlTestFixture, "Table with multiple primary keys", "[DataMappe CHECK(queriedRecords.size() == 1); auto const& queriedRecord = queriedRecords.at(0); INFO("Queried record: " << DataMapper::Inspect(queriedRecord)); + INFO("record: " << DataMapper::Inspect(record)); CHECK(queriedRecord == record); } @@ -159,4 +160,21 @@ TEST_CASE_METHOD(SqlTestFixture, "Loading of the dependent records after create" REQUIRE(!nullableFKUserNotSet.user.Record().has_value()); } +TEST_CASE_METHOD(SqlTestFixture, "Create with defined primary key", "[DataMapper]") +{ + auto dm = DataMapper(); + + dm.CreateTables(); + + auto entry = EntryWithIntPrimaryKey { .id = 42, .comment = "The Answer" }; + dm.Create(entry); + + REQUIRE(entry.id.Value() == 42); + + entry.comment = "Updated Comment"; + dm.Update(entry); + REQUIRE(entry.id.Value() == 42); + REQUIRE(dm.QuerySingle(42).value().comment.Value() == "Updated Comment"); +} + // NOLINTEND(bugprone-unchecked-optional-access) diff --git a/src/tests/DataMapper/Entities.hpp b/src/tests/DataMapper/Entities.hpp index cef09402..234d6975 100644 --- a/src/tests/DataMapper/Entities.hpp +++ b/src/tests/DataMapper/Entities.hpp @@ -133,3 +133,9 @@ struct Appointment return std::weak_ordering::equivalent; } }; + +struct EntryWithIntPrimaryKey +{ + Light::Field id; + Light::Field> comment; +};