diff --git a/src/storage/v2/property_store.cpp b/src/storage/v2/property_store.cpp index 4c08327af..f877e908d 100644 --- a/src/storage/v2/property_store.cpp +++ b/src/storage/v2/property_store.cpp @@ -347,6 +347,17 @@ class Reader { return ReadBytes(reinterpret_cast(data), size); } + bool VerifyBytes(const uint8_t *data, uint64_t size) { + if (pos_ + size > size_) return false; + if (memcmp(data, data_ + pos_, size) != 0) return false; + pos_ += size; + return true; + } + + bool VerifyBytes(const char *data, uint64_t size) { + return VerifyBytes(reinterpret_cast(data), size); + } + bool SkipBytes(uint64_t size) { if (pos_ + size > size_) return false; pos_ += size; @@ -436,6 +447,8 @@ std::optional> EncodePropertyValue( // Function used to decode a PropertyValue from a byte stream. It can either // decode or skip the encoded PropertyValue, depending on the supplied value // pointer. +// +// @sa ComparePropertyValue [[nodiscard]] bool DecodePropertyValue(Reader *reader, Type type, Size payload_size, PropertyValue *value) { @@ -550,6 +563,102 @@ std::optional> EncodePropertyValue( } } +// Function used to compare a PropertyValue to the one stored in the byte +// stream. +// +// NOTE: The logic in this function *MUST* be equal to the logic in +// `PropertyValue::operator==`. If you change this function make sure to change +// the operator so that they have identical functionality. +// +// @sa DecodePropertyValue +[[nodiscard]] bool ComparePropertyValue(Reader *reader, Type type, + Size payload_size, + const PropertyValue &value) { + switch (type) { + case Type::EMPTY: { + return false; + } + case Type::NONE: { + return value.IsNull(); + } + case Type::BOOL: { + if (!value.IsBool()) return false; + bool bool_v = payload_size == Size::INT64; + return value.ValueBool() == bool_v; + } + case Type::INT: { + // Integer and double values are treated as the same in + // `PropertyValue::operator==`. That is why we accept both integer and + // double values here and use the `operator==` between them to verify that + // they are the same. + if (!value.IsInt() && !value.IsDouble()) return false; + auto int_v = reader->ReadInt(payload_size); + if (!int_v) return false; + if (value.IsInt()) { + return value.ValueInt() == int_v; + } else { + return value.ValueDouble() == int_v; + } + } + case Type::DOUBLE: { + // Integer and double values are treated as the same in + // `PropertyValue::operator==`. That is why we accept both integer and + // double values here and use the `operator==` between them to verify that + // they are the same. + if (!value.IsInt() && !value.IsDouble()) return false; + auto double_v = reader->ReadDouble(payload_size); + if (!double_v) return false; + if (value.IsDouble()) { + return value.ValueDouble() == double_v; + } else { + return value.ValueInt() == double_v; + } + } + case Type::STRING: { + if (!value.IsString()) return false; + const auto &str = value.ValueString(); + auto size = reader->ReadUint(payload_size); + if (!size) return false; + if (*size != str.size()) return false; + return reader->VerifyBytes(str.data(), *size); + } + case Type::LIST: { + if (!value.IsList()) return false; + const auto &list = value.ValueList(); + auto size = reader->ReadUint(payload_size); + if (!size) return false; + if (*size != list.size()) return false; + for (uint64_t i = 0; i < *size; ++i) { + auto metadata = reader->ReadMetadata(); + if (!metadata) return false; + if (!ComparePropertyValue(reader, metadata->type, + metadata->payload_size, list[i])) + return false; + } + return true; + } + case Type::MAP: { + if (!value.IsMap()) return false; + const auto &map = value.ValueMap(); + auto size = reader->ReadUint(payload_size); + if (!size) return false; + if (*size != map.size()) return false; + for (const auto &item : map) { + auto metadata = reader->ReadMetadata(); + if (!metadata) return false; + auto key_size = reader->ReadUint(metadata->id_size); + if (!key_size) return false; + if (*key_size != item.first.size()) return false; + if (!reader->VerifyBytes(item.first.data(), *key_size)) return false; + if (!ComparePropertyValue(reader, metadata->type, + metadata->payload_size, item.second)) + return false; + } + return true; + } + } +} + // Function used to encode a property (PropertyId, PropertyValue) into a byte // stream. bool EncodeProperty(Writer *writer, PropertyId property, @@ -593,6 +702,7 @@ enum class DecodeExpectedPropertyStatus { // loaded in this case // // @sa DecodeAnyProperty +// @sa CompareExpectedProperty [[nodiscard]] DecodeExpectedPropertyStatus DecodeExpectedProperty( Reader *reader, PropertyId expected_property, PropertyValue *value) { auto metadata = reader->ReadMetadata(); @@ -602,7 +712,7 @@ enum class DecodeExpectedPropertyStatus { if (!property_id) return DecodeExpectedPropertyStatus::MISSING_DATA; // Don't load the value if this isn't the expected property. - if (property_id != expected_property.AsUint()) { + if (*property_id != expected_property.AsUint()) { value = nullptr; } @@ -610,9 +720,9 @@ enum class DecodeExpectedPropertyStatus { value)) return DecodeExpectedPropertyStatus::MISSING_DATA; - if (property_id < expected_property.AsUint()) { + if (*property_id < expected_property.AsUint()) { return DecodeExpectedPropertyStatus::SMALLER; - } else if (property_id == expected_property.AsUint()) { + } else if (*property_id == expected_property.AsUint()) { return DecodeExpectedPropertyStatus::EQUAL; } else { return DecodeExpectedPropertyStatus::GREATER; @@ -624,6 +734,7 @@ enum class DecodeExpectedPropertyStatus { // the provided PropertyValue. // // @sa DecodeExpectedProperty +// @sa CompareExpectedProperty [[nodiscard]] std::optional DecodeAnyProperty( Reader *reader, PropertyValue *value) { auto metadata = reader->ReadMetadata(); @@ -639,6 +750,25 @@ enum class DecodeExpectedPropertyStatus { return PropertyId::FromUint(*property_id); } +// Function used to compare a property (PropertyId, PropertyValue) to current +// property in the byte stream. +// +// @sa DecodeExpectedProperty +// @sa DecodeAnyProperty +[[nodiscard]] bool CompareExpectedProperty(Reader *reader, + PropertyId expected_property, + const PropertyValue &value) { + auto metadata = reader->ReadMetadata(); + if (!metadata) return false; + + auto property_id = reader->ReadUint(metadata->id_size); + if (!property_id) return false; + if (*property_id != expected_property.AsUint()) return false; + + return ComparePropertyValue(reader, metadata->type, metadata->payload_size, + value); +} + // Function used to find and (selectively) get the property value of the // property whose ID is `property`. It relies on the fact that the properties // are sorted (by ID) in the buffer. If the function doesn't find the property, @@ -824,6 +954,24 @@ bool PropertyStore::HasProperty(PropertyId property) const { DecodeExpectedPropertyStatus::EQUAL; } +bool PropertyStore::IsPropertyEqual(PropertyId property, + const PropertyValue &value) const { + uint64_t size; + const uint8_t *data; + std::tie(size, data) = GetSizeData(buffer_); + if (size % 8 != 0) { + // We are storing the data in the local buffer. + size = sizeof(buffer_) - 1; + data = &buffer_[1]; + } + Reader reader(data, size); + auto info = FindSpecificPropertyAndBufferInfo(&reader, property); + if (info.property_size == 0) return value.IsNull(); + Reader prop_reader(data + info.property_begin, info.property_size); + if (!CompareExpectedProperty(&prop_reader, property, value)) return false; + return prop_reader.GetPosition() == info.property_size; +} + std::map PropertyStore::Properties() const { uint64_t size; const uint8_t *data; diff --git a/src/storage/v2/property_store.hpp b/src/storage/v2/property_store.hpp index afbebffcb..5cd371c23 100644 --- a/src/storage/v2/property_store.hpp +++ b/src/storage/v2/property_store.hpp @@ -28,6 +28,12 @@ class PropertyStore { /// complexity of this function is O(n). bool HasProperty(PropertyId property) const; + /// Checks whether the property `property` is equal to the specified value + /// `value`. This function doesn't perform any memory allocations while + /// performing the equality check. The time complexity of this function is + /// O(n). + bool IsPropertyEqual(PropertyId property, const PropertyValue &value) const; + /// Returns all properties currently stored in the store. The time complexity /// of this function is O(n). /// @throw std::bad_alloc diff --git a/src/storage/v2/property_value.hpp b/src/storage/v2/property_value.hpp index cd4e8916e..0522dfb19 100644 --- a/src/storage/v2/property_value.hpp +++ b/src/storage/v2/property_value.hpp @@ -243,7 +243,9 @@ inline std::ostream &operator<<(std::ostream &os, const PropertyValue &value) { } } -// comparison +// NOTE: The logic in this function *MUST* be equal to the logic in +// `PropertyStore::ComparePropertyValue`. If you change this operator make sure +// to change the function so that they have identical functionality. inline bool operator==(const PropertyValue &first, const PropertyValue &second) noexcept { if (!PropertyValue::AreComparableTypes(first.type(), second.type())) diff --git a/tests/unit/storage_v2_property_store.cpp b/tests/unit/storage_v2_property_store.cpp index 1da854b49..c1ae85a26 100644 --- a/tests/unit/storage_v2_property_store.cpp +++ b/tests/unit/storage_v2_property_store.cpp @@ -7,6 +7,54 @@ using testing::UnorderedElementsAre; +const storage::PropertyValue kSampleValues[] = { + storage::PropertyValue(), + storage::PropertyValue(false), + storage::PropertyValue(true), + storage::PropertyValue(0), + storage::PropertyValue(33), + storage::PropertyValue(-33), + storage::PropertyValue(-3137), + storage::PropertyValue(3137), + storage::PropertyValue(310000007), + storage::PropertyValue(-310000007), + storage::PropertyValue(3100000000007L), + storage::PropertyValue(-3100000000007L), + storage::PropertyValue(0.0), + storage::PropertyValue(33.33), + storage::PropertyValue(-33.33), + storage::PropertyValue(3137.3137), + storage::PropertyValue(-3137.3137), + storage::PropertyValue("sample"), + storage::PropertyValue(std::string(404, 'n')), + storage::PropertyValue(std::vector{ + storage::PropertyValue(33), + storage::PropertyValue(std::string("sample")), + storage::PropertyValue(-33.33)}), + storage::PropertyValue(std::vector{ + storage::PropertyValue(), storage::PropertyValue(false)}), + storage::PropertyValue(std::map{ + {"sample", storage::PropertyValue()}, + {"key", storage::PropertyValue(false)}}), + storage::PropertyValue(std::map{ + {"test", storage::PropertyValue(33)}, + {"map", storage::PropertyValue(std::string("sample"))}, + {"item", storage::PropertyValue(-33.33)}}), +}; + +void TestIsPropertyEqual(const storage::PropertyStore &store, + storage::PropertyId property, + const storage::PropertyValue &value) { + ASSERT_TRUE(store.IsPropertyEqual(property, value)); + for (const auto &sample : kSampleValues) { + if (sample == value) { + ASSERT_TRUE(store.IsPropertyEqual(property, sample)); + } else { + ASSERT_FALSE(store.IsPropertyEqual(property, sample)); + } + } +} + TEST(PropertyStore, Simple) { storage::PropertyStore props; auto prop = storage::PropertyId::FromInt(42); @@ -14,11 +62,13 @@ TEST(PropertyStore, Simple) { ASSERT_TRUE(props.SetProperty(prop, value)); ASSERT_EQ(props.GetProperty(prop), value); ASSERT_TRUE(props.HasProperty(prop)); + TestIsPropertyEqual(props, prop, value); ASSERT_THAT(props.Properties(), UnorderedElementsAre(std::pair(prop, value))); ASSERT_FALSE(props.SetProperty(prop, storage::PropertyValue())); ASSERT_TRUE(props.GetProperty(prop).IsNull()); ASSERT_FALSE(props.HasProperty(prop)); + TestIsPropertyEqual(props, prop, storage::PropertyValue()); ASSERT_EQ(props.Properties().size(), 0); } @@ -29,11 +79,13 @@ TEST(PropertyStore, SimpleLarge) { ASSERT_TRUE(props.SetProperty(prop, value)); ASSERT_EQ(props.GetProperty(prop), value); ASSERT_TRUE(props.HasProperty(prop)); + TestIsPropertyEqual(props, prop, value); ASSERT_THAT(props.Properties(), UnorderedElementsAre(std::pair(prop, value))); ASSERT_FALSE(props.SetProperty(prop, storage::PropertyValue())); ASSERT_TRUE(props.GetProperty(prop).IsNull()); ASSERT_FALSE(props.HasProperty(prop)); + TestIsPropertyEqual(props, prop, storage::PropertyValue()); ASSERT_EQ(props.Properties().size(), 0); } @@ -43,6 +95,7 @@ TEST(PropertyStore, EmptySetToNull) { ASSERT_TRUE(props.SetProperty(prop, storage::PropertyValue())); ASSERT_TRUE(props.GetProperty(prop).IsNull()); ASSERT_FALSE(props.HasProperty(prop)); + TestIsPropertyEqual(props, prop, storage::PropertyValue()); ASSERT_EQ(props.Properties().size(), 0); } @@ -53,10 +106,12 @@ TEST(PropertyStore, Clear) { ASSERT_TRUE(props.SetProperty(prop, value)); ASSERT_EQ(props.GetProperty(prop), value); ASSERT_TRUE(props.HasProperty(prop)); + TestIsPropertyEqual(props, prop, value); ASSERT_THAT(props.Properties(), UnorderedElementsAre(std::pair(prop, value))); ASSERT_TRUE(props.ClearProperties()); ASSERT_TRUE(props.GetProperty(prop).IsNull()); ASSERT_FALSE(props.HasProperty(prop)); + TestIsPropertyEqual(props, prop, storage::PropertyValue()); ASSERT_EQ(props.Properties().size(), 0); } @@ -73,18 +128,21 @@ TEST(PropertyStore, MoveConstruct) { ASSERT_TRUE(props1.SetProperty(prop, value)); ASSERT_EQ(props1.GetProperty(prop), value); ASSERT_TRUE(props1.HasProperty(prop)); + TestIsPropertyEqual(props1, prop, value); ASSERT_THAT(props1.Properties(), UnorderedElementsAre(std::pair(prop, value))); { storage::PropertyStore props2(std::move(props1)); ASSERT_EQ(props2.GetProperty(prop), value); ASSERT_TRUE(props2.HasProperty(prop)); + TestIsPropertyEqual(props2, prop, value); ASSERT_THAT(props2.Properties(), UnorderedElementsAre(std::pair(prop, value))); } // NOLINTNEXTLINE(bugprone-use-after-move,clang-analyzer-cplusplus.Move,hicpp-invalid-access-moved) ASSERT_TRUE(props1.GetProperty(prop).IsNull()); ASSERT_FALSE(props1.HasProperty(prop)); + TestIsPropertyEqual(props1, prop, storage::PropertyValue()); ASSERT_EQ(props1.Properties().size(), 0); } @@ -95,18 +153,21 @@ TEST(PropertyStore, MoveConstructLarge) { ASSERT_TRUE(props1.SetProperty(prop, value)); ASSERT_EQ(props1.GetProperty(prop), value); ASSERT_TRUE(props1.HasProperty(prop)); + TestIsPropertyEqual(props1, prop, value); ASSERT_THAT(props1.Properties(), UnorderedElementsAre(std::pair(prop, value))); { storage::PropertyStore props2(std::move(props1)); ASSERT_EQ(props2.GetProperty(prop), value); ASSERT_TRUE(props2.HasProperty(prop)); + TestIsPropertyEqual(props2, prop, value); ASSERT_THAT(props2.Properties(), UnorderedElementsAre(std::pair(prop, value))); } // NOLINTNEXTLINE(bugprone-use-after-move,clang-analyzer-cplusplus.Move,hicpp-invalid-access-moved) ASSERT_TRUE(props1.GetProperty(prop).IsNull()); ASSERT_FALSE(props1.HasProperty(prop)); + TestIsPropertyEqual(props1, prop, storage::PropertyValue()); ASSERT_EQ(props1.Properties().size(), 0); } @@ -117,6 +178,7 @@ TEST(PropertyStore, MoveAssign) { ASSERT_TRUE(props1.SetProperty(prop, value)); ASSERT_EQ(props1.GetProperty(prop), value); ASSERT_TRUE(props1.HasProperty(prop)); + TestIsPropertyEqual(props1, prop, value); ASSERT_THAT(props1.Properties(), UnorderedElementsAre(std::pair(prop, value))); { @@ -125,17 +187,20 @@ TEST(PropertyStore, MoveAssign) { ASSERT_TRUE(props2.SetProperty(prop, value2)); ASSERT_EQ(props2.GetProperty(prop), value2); ASSERT_TRUE(props2.HasProperty(prop)); + TestIsPropertyEqual(props2, prop, value2); ASSERT_THAT(props2.Properties(), UnorderedElementsAre(std::pair(prop, value2))); props2 = std::move(props1); ASSERT_EQ(props2.GetProperty(prop), value); ASSERT_TRUE(props2.HasProperty(prop)); + TestIsPropertyEqual(props2, prop, value); ASSERT_THAT(props2.Properties(), UnorderedElementsAre(std::pair(prop, value))); } // NOLINTNEXTLINE(bugprone-use-after-move,clang-analyzer-cplusplus.Move,hicpp-invalid-access-moved) ASSERT_TRUE(props1.GetProperty(prop).IsNull()); ASSERT_FALSE(props1.HasProperty(prop)); + TestIsPropertyEqual(props1, prop, storage::PropertyValue()); ASSERT_EQ(props1.Properties().size(), 0); } @@ -146,6 +211,7 @@ TEST(PropertyStore, MoveAssignLarge) { ASSERT_TRUE(props1.SetProperty(prop, value)); ASSERT_EQ(props1.GetProperty(prop), value); ASSERT_TRUE(props1.HasProperty(prop)); + TestIsPropertyEqual(props1, prop, value); ASSERT_THAT(props1.Properties(), UnorderedElementsAre(std::pair(prop, value))); { @@ -154,17 +220,20 @@ TEST(PropertyStore, MoveAssignLarge) { ASSERT_TRUE(props2.SetProperty(prop, value2)); ASSERT_EQ(props2.GetProperty(prop), value2); ASSERT_TRUE(props2.HasProperty(prop)); + TestIsPropertyEqual(props2, prop, value2); ASSERT_THAT(props2.Properties(), UnorderedElementsAre(std::pair(prop, value2))); props2 = std::move(props1); ASSERT_EQ(props2.GetProperty(prop), value); ASSERT_TRUE(props2.HasProperty(prop)); + TestIsPropertyEqual(props2, prop, value); ASSERT_THAT(props2.Properties(), UnorderedElementsAre(std::pair(prop, value))); } // NOLINTNEXTLINE(bugprone-use-after-move,clang-analyzer-cplusplus.Move,hicpp-invalid-access-moved) ASSERT_TRUE(props1.GetProperty(prop).IsNull()); ASSERT_FALSE(props1.HasProperty(prop)); + TestIsPropertyEqual(props1, prop, storage::PropertyValue()); ASSERT_EQ(props1.Properties().size(), 0); } @@ -186,20 +255,24 @@ TEST(PropertyStore, EmptySet) { ASSERT_TRUE(props.SetProperty(prop, value)); ASSERT_EQ(props.GetProperty(prop), value); ASSERT_TRUE(props.HasProperty(prop)); + TestIsPropertyEqual(props, prop, value); ASSERT_THAT(props.Properties(), UnorderedElementsAre(std::pair(prop, value))); ASSERT_FALSE(props.SetProperty(prop, value)); ASSERT_EQ(props.GetProperty(prop), value); ASSERT_TRUE(props.HasProperty(prop)); + TestIsPropertyEqual(props, prop, value); ASSERT_THAT(props.Properties(), UnorderedElementsAre(std::pair(prop, value))); ASSERT_FALSE(props.SetProperty(prop, storage::PropertyValue())); ASSERT_TRUE(props.GetProperty(prop).IsNull()); ASSERT_FALSE(props.HasProperty(prop)); + TestIsPropertyEqual(props, prop, storage::PropertyValue()); ASSERT_EQ(props.Properties().size(), 0); ASSERT_TRUE(props.SetProperty(prop, storage::PropertyValue())); ASSERT_TRUE(props.GetProperty(prop).IsNull()); ASSERT_FALSE(props.HasProperty(prop)); + TestIsPropertyEqual(props, prop, storage::PropertyValue()); ASSERT_EQ(props.Properties().size(), 0); } } @@ -247,9 +320,11 @@ TEST(PropertyStore, FullSet) { } else { ASSERT_TRUE(props.HasProperty(item.first)); } + TestIsPropertyEqual(props, item.first, alt[i]); } else { ASSERT_EQ(props.GetProperty(item.first), item.second); ASSERT_TRUE(props.HasProperty(item.first)); + TestIsPropertyEqual(props, item.first, item.second); } } auto current = data; @@ -271,9 +346,11 @@ TEST(PropertyStore, FullSet) { } else { ASSERT_TRUE(props.HasProperty(item.first)); } + TestIsPropertyEqual(props, item.first, alt[i]); } else { ASSERT_EQ(props.GetProperty(item.first), item.second); ASSERT_TRUE(props.HasProperty(item.first)); + TestIsPropertyEqual(props, item.first, item.second); } } auto current = data; @@ -288,12 +365,14 @@ TEST(PropertyStore, FullSet) { ASSERT_TRUE(props.SetProperty(target.first, target.second)); ASSERT_EQ(props.GetProperty(target.first), target.second); ASSERT_TRUE(props.HasProperty(target.first)); + TestIsPropertyEqual(props, target.first, target.second); props.ClearProperties(); ASSERT_EQ(props.Properties().size(), 0); for (const auto &item : data) { ASSERT_TRUE(props.GetProperty(item.first).IsNull()); ASSERT_FALSE(props.HasProperty(item.first)); + TestIsPropertyEqual(props, item.first, storage::PropertyValue()); } } } @@ -343,12 +422,14 @@ TEST(PropertyStore, IntEncoding) { ASSERT_TRUE(props.SetProperty(item.first, item.second)); ASSERT_EQ(props.GetProperty(item.first), item.second); ASSERT_TRUE(props.HasProperty(item.first)); + TestIsPropertyEqual(props, item.first, item.second); } for (auto it = data.rbegin(); it != data.rend(); ++it) { const auto &item = *it; ASSERT_FALSE(props.SetProperty(item.first, item.second)); ASSERT_EQ(props.GetProperty(item.first), item.second); ASSERT_TRUE(props.HasProperty(item.first)); + TestIsPropertyEqual(props, item.first, item.second); } ASSERT_EQ(props.Properties(), data); @@ -358,5 +439,207 @@ TEST(PropertyStore, IntEncoding) { for (const auto &item : data) { ASSERT_TRUE(props.GetProperty(item.first).IsNull()); ASSERT_FALSE(props.HasProperty(item.first)); + TestIsPropertyEqual(props, item.first, storage::PropertyValue()); } } + +TEST(PropertyStore, IsPropertyEqualIntAndDouble) { + storage::PropertyStore props; + auto prop = storage::PropertyId::FromInt(42); + + ASSERT_TRUE(props.SetProperty(prop, storage::PropertyValue(42))); + + std::vector> tests{ + {storage::PropertyValue(0), storage::PropertyValue(0.0)}, + {storage::PropertyValue(123), storage::PropertyValue(123.0)}, + {storage::PropertyValue(12345), storage::PropertyValue(12345.0)}, + {storage::PropertyValue(12345678), storage::PropertyValue(12345678.0)}, + {storage::PropertyValue(1234567890123L), + storage::PropertyValue(1234567890123.0)}, + }; + + // Test equality with raw values. + for (auto test : tests) { + ASSERT_EQ(test.first, test.second); + + // Test first, second + ASSERT_FALSE(props.SetProperty(prop, test.first)); + ASSERT_EQ(props.GetProperty(prop), test.first); + ASSERT_TRUE(props.HasProperty(prop)); + ASSERT_TRUE(props.IsPropertyEqual(prop, test.first)); + ASSERT_TRUE(props.IsPropertyEqual(prop, test.second)); + + // Test second, first + ASSERT_FALSE(props.SetProperty(prop, test.second)); + ASSERT_EQ(props.GetProperty(prop), test.second); + ASSERT_TRUE(props.HasProperty(prop)); + ASSERT_TRUE(props.IsPropertyEqual(prop, test.second)); + ASSERT_TRUE(props.IsPropertyEqual(prop, test.first)); + + // Make both negative + test.first = storage::PropertyValue(test.first.ValueInt() * -1); + test.second = storage::PropertyValue(test.second.ValueDouble() * -1.0); + ASSERT_EQ(test.first, test.second); + + // Test -first, -second + ASSERT_FALSE(props.SetProperty(prop, test.first)); + ASSERT_EQ(props.GetProperty(prop), test.first); + ASSERT_TRUE(props.HasProperty(prop)); + ASSERT_TRUE(props.IsPropertyEqual(prop, test.first)); + ASSERT_TRUE(props.IsPropertyEqual(prop, test.second)); + + // Test -second, -first + ASSERT_FALSE(props.SetProperty(prop, test.second)); + ASSERT_EQ(props.GetProperty(prop), test.second); + ASSERT_TRUE(props.HasProperty(prop)); + ASSERT_TRUE(props.IsPropertyEqual(prop, test.second)); + ASSERT_TRUE(props.IsPropertyEqual(prop, test.first)); + } + + // Test equality with values wrapped in lists. + for (auto test : tests) { + test.first = storage::PropertyValue(std::vector{ + storage::PropertyValue(test.first.ValueInt())}); + test.second = storage::PropertyValue(std::vector{ + storage::PropertyValue(test.second.ValueDouble())}); + ASSERT_EQ(test.first, test.second); + + // Test first, second + ASSERT_FALSE(props.SetProperty(prop, test.first)); + ASSERT_EQ(props.GetProperty(prop), test.first); + ASSERT_TRUE(props.HasProperty(prop)); + ASSERT_TRUE(props.IsPropertyEqual(prop, test.first)); + ASSERT_TRUE(props.IsPropertyEqual(prop, test.second)); + + // Test second, first + ASSERT_FALSE(props.SetProperty(prop, test.second)); + ASSERT_EQ(props.GetProperty(prop), test.second); + ASSERT_TRUE(props.HasProperty(prop)); + ASSERT_TRUE(props.IsPropertyEqual(prop, test.second)); + ASSERT_TRUE(props.IsPropertyEqual(prop, test.first)); + + // Make both negative + test.first = storage::PropertyValue(std::vector{ + storage::PropertyValue(test.first.ValueList()[0].ValueInt() * -1)}); + test.second = storage::PropertyValue( + std::vector{storage::PropertyValue( + test.second.ValueList()[0].ValueDouble() * -1.0)}); + ASSERT_EQ(test.first, test.second); + + // Test -first, -second + ASSERT_FALSE(props.SetProperty(prop, test.first)); + ASSERT_EQ(props.GetProperty(prop), test.first); + ASSERT_TRUE(props.HasProperty(prop)); + ASSERT_TRUE(props.IsPropertyEqual(prop, test.first)); + ASSERT_TRUE(props.IsPropertyEqual(prop, test.second)); + + // Test -second, -first + ASSERT_FALSE(props.SetProperty(prop, test.second)); + ASSERT_EQ(props.GetProperty(prop), test.second); + ASSERT_TRUE(props.HasProperty(prop)); + ASSERT_TRUE(props.IsPropertyEqual(prop, test.second)); + ASSERT_TRUE(props.IsPropertyEqual(prop, test.first)); + } +} + +TEST(PropertyStore, IsPropertyEqualString) { + storage::PropertyStore props; + auto prop = storage::PropertyId::FromInt(42); + ASSERT_TRUE(props.SetProperty(prop, storage::PropertyValue("test"))); + ASSERT_TRUE(props.IsPropertyEqual(prop, storage::PropertyValue("test"))); + + // Different length. + ASSERT_FALSE( + props.IsPropertyEqual(prop, storage::PropertyValue("helloworld"))); + + // Same length, different value. + ASSERT_FALSE(props.IsPropertyEqual(prop, storage::PropertyValue("asdf"))); + + // Shortened and extended. + ASSERT_FALSE(props.IsPropertyEqual(prop, storage::PropertyValue("tes"))); + ASSERT_FALSE(props.IsPropertyEqual(prop, storage::PropertyValue("testt"))); +} + +TEST(PropertyStore, IsPropertyEqualList) { + storage::PropertyStore props; + auto prop = storage::PropertyId::FromInt(42); + ASSERT_TRUE(props.SetProperty( + prop, storage::PropertyValue(std::vector{ + storage::PropertyValue(42), storage::PropertyValue("test")}))); + ASSERT_TRUE(props.IsPropertyEqual( + prop, storage::PropertyValue(std::vector{ + storage::PropertyValue(42), storage::PropertyValue("test")}))); + + // Different length. + ASSERT_FALSE(props.IsPropertyEqual( + prop, storage::PropertyValue(std::vector{ + storage::PropertyValue(24)}))); + + // Same length, different value. + ASSERT_FALSE(props.IsPropertyEqual( + prop, storage::PropertyValue(std::vector{ + storage::PropertyValue(42), storage::PropertyValue("asdf")}))); + + // Shortened and extended. + ASSERT_FALSE(props.IsPropertyEqual( + prop, storage::PropertyValue(std::vector{ + storage::PropertyValue(42)}))); + ASSERT_FALSE(props.IsPropertyEqual( + prop, storage::PropertyValue(std::vector{ + storage::PropertyValue(42), storage::PropertyValue("test"), + storage::PropertyValue(true)}))); +} + +TEST(PropertyStore, IsPropertyEqualMap) { + storage::PropertyStore props; + auto prop = storage::PropertyId::FromInt(42); + ASSERT_TRUE(props.SetProperty( + prop, + storage::PropertyValue(std::map{ + {"abc", storage::PropertyValue(42)}, + {"zyx", storage::PropertyValue("test")}}))); + ASSERT_TRUE(props.IsPropertyEqual( + prop, + storage::PropertyValue(std::map{ + {"abc", storage::PropertyValue(42)}, + {"zyx", storage::PropertyValue("test")}}))); + + // Different length. + ASSERT_FALSE(props.IsPropertyEqual( + prop, + storage::PropertyValue(std::map{ + {"fgh", storage::PropertyValue(24)}}))); + + // Same length, different value. + ASSERT_FALSE(props.IsPropertyEqual( + prop, + storage::PropertyValue(std::map{ + {"abc", storage::PropertyValue(42)}, + {"zyx", storage::PropertyValue("testt")}}))); + + // Same length, different key (different length). + ASSERT_FALSE(props.IsPropertyEqual( + prop, + storage::PropertyValue(std::map{ + {"abc", storage::PropertyValue(42)}, + {"zyxw", storage::PropertyValue("test")}}))); + + // Same length, different key (same length). + ASSERT_FALSE(props.IsPropertyEqual( + prop, + storage::PropertyValue(std::map{ + {"abc", storage::PropertyValue(42)}, + {"zyw", storage::PropertyValue("test")}}))); + + // Shortened and extended. + ASSERT_FALSE(props.IsPropertyEqual( + prop, + storage::PropertyValue(std::map{ + {"abc", storage::PropertyValue(42)}}))); + ASSERT_FALSE(props.IsPropertyEqual( + prop, + storage::PropertyValue(std::map{ + {"abc", storage::PropertyValue(42)}, + {"sdf", storage::PropertyValue(true)}, + {"zyx", storage::PropertyValue("test")}}))); +}