Files
memgraph/src/storage/v2/property_store.cpp
Gareth Andrew Lloyd 1bd47318cd Improve PropertyStore (#1142)
Improve AnyVersionHasLabelProperty by doing less work in some instances.
Improve FindSpecificProperty.
2023-09-09 08:00:43 -04:00

1399 lines
48 KiB
C++

// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
// License, and you may not use this file except in compliance with the Business Source License.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
#include "storage/v2/property_store.hpp"
#include <cstdint>
#include <cstring>
#include <iterator>
#include <limits>
#include <optional>
#include <sstream>
#include <tuple>
#include <type_traits>
#include <utility>
#include "storage/v2/temporal.hpp"
#include "utils/cast.hpp"
#include "utils/logging.hpp"
namespace memgraph::storage {
namespace {
// `PropertyValue` is a very large object. It is implemented as a `union` of all
// possible types that could be stored as a property value. That causes the
// object to be 50+ bytes in size. Many use-cases only use primitive property
// types (such as booleans, integers and doubles). When storing an integer in
// the `PropertyValue` there is a lot of memory being wasted (40+ bytes). For
// boolean values the memory wastage is even worse (almost all of the 50+
// bytes). Also, the `PropertyValue` must have a `type` member that (even though
// it is small) causes a padding hole to be inserted in the `PropertyValue` that
// wastes even more memory. Memory is wasted even more when the `PropertyValue`
// stores a list or a map of `PropertyValue`s because each of the internal
// values also wastes memory.
//
// Even though there is a lot of memory being wasted in `PropertyValue`, all of
// the data structures used inside it enable very fast object traversal because
// there is no additional decoding that should be done. Time complexity of all
// functions used to access compound values is very good (usually O(log(n))).
//
// Because the values stored in a vertex or edge must be reconstructed
// (specifically, they must be copied) every time a property is accessed or
// modified (because of the MVCC implementation) it makes sense to optimize the
// data structure that is used as permanent storage of the property values in
// vertices and edges.
//
// The `PropertyStore` is used to provide a very efficient means of permanently
// storing a map of `PropertyId` to `PropertyValue` mappings. It reduces memory
// usage with the cost of a higher time complexity of the operations that
// access the store. Compared to a `std::map<PropertyValue>`, the
// `PropertyStore` uses approximately 10 times less memory. But, the time
// complexity of its get and set operations is O(n) instead of O(log(n)).
//
// The values themselves are stored encoded in a flat buffer. On an insertion
// the underlying storage buffer is resized if necessary and on removal the
// underlying storage buffer is shrinked if the new data can fit into a buffer
// that is 1/3 smaller than the current buffer. If it can't fit into a smaller
// buffer, the current buffer is used. All mappings are encoded independently of
// each other.
//
// Each mapping starts with an encoded metadata field that is used for several
// purposes:
// * to determine the encoded type
// * to determine the encoded property ID size
// * to determine the encoded payload size
//
// The metadata field is always a single byte and its bits are used as follows:
// 0b0000 0000
// ++++ -> type (4 bits)
// ++ -> size of property ID (2 bits)
// ++ -> size of payload OR size of payload size indicator (2 bits)
//
// When encoding integers (`int64_t` and `uint64_t`) they are compressed so that
// they are stored into 1, 2, 4 or 8 bytes depending on their value.
//
// The size of the metadata field is very important because it is encoded with
// each and every ID to value mapping. That is why every possible bit is used
// to store some useful information. Increasing the size of the metadata field
// will increase memory usage for every stored ID to value mapping.
enum class Size : uint8_t {
INT8 = 0x00,
INT16 = 0x01,
INT32 = 0x02,
INT64 = 0x03,
};
// All of these values must have the lowest 4 bits set to zero because they are
// used to store two `Size` values as described in the comment above.
enum class Type : uint8_t {
EMPTY = 0x00, // Special value used to indicate end of buffer.
NONE = 0x10, // NONE used instead of NULL because NULL is defined to
// something...
BOOL = 0x20,
INT = 0x30,
DOUBLE = 0x40,
STRING = 0x50,
LIST = 0x60,
MAP = 0x70,
TEMPORAL_DATA = 0x80
};
const uint8_t kMaskType = 0xf0;
const uint8_t kMaskIdSize = 0x0c;
const uint8_t kMaskPayloadSize = 0x03;
const uint8_t kShiftIdSize = 2;
// Values are encoded as follows:
// * NULL
// - type; payload size is not used
// * BOOL
// - type; payload size is used as value
// - encoded property ID
// * INT
// - type; payload size is used to indicate whether the value is encoded as
// `int8_t`, `int16_t`, `int32_t` or `int64_t`
// - encoded property ID
// - encoded property value
// * DOUBLE
// - type; payload size isn't used
// - encoded property ID
// - encoded value
// * STRING
// - type; payload size is used to indicate whether the string size is
// encoded as `uint8_t`, `uint16_t`, `uint32_t` or `uint64_t`
// - encoded property ID
// - encoded string size
// - string data
// * LIST
// - type; payload size is used to indicate whether the list size is encoded
// as `uint8_t`, `uint16_t`, `uint32_t` or `uint64_t`
// - encoded property ID
// - encoded list size
// - list items
// + type; id size is not used; payload size is used to indicate the size
// of the item
// + encoded item size
// + encoded item data
// * MAP
// - type; payload size is used to indicate whether the map size is encoded
// as `uint8_t`, `uint16_t`, `uint32_t` or `uint64_t`
// - encoded property ID
// - encoded map size
// - map items
// + type; id size is used to indicate whether the key size is encoded as
// `uint8_t`, `uint16_t`, `uint32_t` or `uint64_t`; payload size is used
// as described above for the inner payload type
// + encoded key size
// + encoded key data
// + encoded value size
// + encoded value data
// * TEMPORAL_DATE
// - type; payload size isn't used
// - encoded property ID
// - value saved as Metadata
// + type; id size is used to indicate whether the temporal data type is encoded
// as `uint8_t`, `uint16_t`, `uint32_t` or `uint64_t`; payload size used to
// indicate whether the microseconds are encoded as `uint8_t`, `uint16_t, `uint32_t
// or `uint64_t`
// + encoded temporal data type value
// + encoded microseconds value
struct Metadata {
Type type{Type::EMPTY};
Size id_size{Size::INT8};
Size payload_size{Size::INT8};
};
// Helper class used to write data to the binary stream.
class Writer {
public:
class MetadataHandle {
public:
MetadataHandle() {}
explicit MetadataHandle(uint8_t *value) : value_(value) {}
void Set(Metadata metadata) {
if (!value_) return;
auto value = static_cast<uint8_t>(metadata.type);
value |= static_cast<uint8_t>(static_cast<uint8_t>(metadata.id_size) << kShiftIdSize);
value |= static_cast<uint8_t>(metadata.payload_size);
*value_ = value;
}
private:
uint8_t *value_{nullptr};
};
Writer() {}
Writer(uint8_t *data, uint64_t size) : data_(data), size_(size) {}
std::optional<MetadataHandle> WriteMetadata() {
if (data_ && pos_ + 1 > size_) return std::nullopt;
MetadataHandle handle;
if (data_) handle = MetadataHandle(&data_[pos_]);
++pos_;
return handle;
}
std::optional<Size> WriteInt(int64_t value) {
if (InternalWriteInt<int8_t>(value)) {
return Size::INT8;
} else if (InternalWriteInt<int16_t>(value)) {
return Size::INT16;
} else if (InternalWriteInt<int32_t>(value)) {
return Size::INT32;
} else if (InternalWriteInt<int64_t>(value)) {
return Size::INT64;
} else {
return std::nullopt;
}
}
std::optional<Size> WriteUint(uint64_t value) {
if (InternalWriteInt<uint8_t>(value)) {
return Size::INT8;
} else if (InternalWriteInt<uint16_t>(value)) {
return Size::INT16;
} else if (InternalWriteInt<uint32_t>(value)) {
return Size::INT32;
} else if (InternalWriteInt<uint64_t>(value)) {
return Size::INT64;
} else {
return std::nullopt;
}
}
std::optional<Size> WriteDouble(double value) { return WriteUint(utils::MemcpyCast<uint64_t>(value)); }
bool WriteBytes(const uint8_t *data, uint64_t size) {
if (data_ && pos_ + size > size_) return false;
if (data_) memcpy(data_ + pos_, data, size);
pos_ += size;
return true;
}
bool WriteBytes(const char *data, uint64_t size) {
static_assert(std::is_same_v<uint8_t, unsigned char>);
return WriteBytes(reinterpret_cast<const uint8_t *>(data), size);
}
uint64_t Written() const { return pos_; }
private:
template <typename T, typename V>
bool InternalWriteInt(V value) {
static_assert(std::numeric_limits<T>::is_integer);
static_assert(std::numeric_limits<V>::is_integer);
static_assert(std::numeric_limits<T>::is_signed == std::numeric_limits<V>::is_signed);
if (value < std::numeric_limits<T>::min() || value > std::numeric_limits<T>::max()) return false;
if (data_ && pos_ + sizeof(T) > size_) return false;
T tmp = value;
if (data_) memcpy(data_ + pos_, &tmp, sizeof(T));
pos_ += sizeof(T);
return true;
}
uint8_t *data_{nullptr};
uint64_t size_{0};
uint64_t pos_{0};
};
// Helper class used to read data from the binary stream.
class Reader {
public:
Reader(const uint8_t *data, uint64_t size) : data_(data), size_(size), pos_(0) {}
std::optional<Metadata> ReadMetadata() {
if (pos_ + 1 > size_) return std::nullopt;
uint8_t value = data_[pos_++];
Metadata metadata;
metadata.type = static_cast<Type>(value & kMaskType);
metadata.id_size = static_cast<Size>(static_cast<uint8_t>(value & kMaskIdSize) >> kShiftIdSize);
metadata.payload_size = static_cast<Size>(value & kMaskPayloadSize);
return metadata;
}
std::optional<int64_t> ReadInt(Size size) {
int64_t ret = 0;
switch (size) {
case Size::INT8: {
auto value = InternalReadInt<int8_t>();
if (!value) return std::nullopt;
ret = *value;
break;
}
case Size::INT16: {
auto value = InternalReadInt<int16_t>();
if (!value) return std::nullopt;
ret = *value;
break;
}
case Size::INT32: {
auto value = InternalReadInt<int32_t>();
if (!value) return std::nullopt;
ret = *value;
break;
}
case Size::INT64: {
auto value = InternalReadInt<int64_t>();
if (!value) return std::nullopt;
ret = *value;
break;
}
}
return ret;
}
std::optional<uint64_t> ReadUint(Size size) {
uint64_t ret = 0;
switch (size) {
case Size::INT8: {
auto value = InternalReadInt<uint8_t>();
if (!value) return std::nullopt;
ret = *value;
break;
}
case Size::INT16: {
auto value = InternalReadInt<uint16_t>();
if (!value) return std::nullopt;
ret = *value;
break;
}
case Size::INT32: {
auto value = InternalReadInt<uint32_t>();
if (!value) return std::nullopt;
ret = *value;
break;
}
case Size::INT64: {
auto value = InternalReadInt<uint64_t>();
if (!value) return std::nullopt;
ret = *value;
break;
}
}
return ret;
}
std::optional<double> ReadDouble(Size size) {
auto value = ReadUint(size);
if (!value) return std::nullopt;
return utils::MemcpyCast<double>(*value);
}
bool ReadBytes(uint8_t *data, uint64_t size) {
if (pos_ + size > size_) return false;
memcpy(data, data_ + pos_, size);
pos_ += size;
return true;
}
bool ReadBytes(char *data, uint64_t size) { return ReadBytes(reinterpret_cast<uint8_t *>(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<const uint8_t *>(data), size);
}
bool SkipBytes(uint64_t size) {
if (pos_ + size > size_) return false;
pos_ += size;
return true;
}
uint64_t GetPosition() const { return pos_; }
private:
template <typename T>
std::optional<T> InternalReadInt() {
if (pos_ + sizeof(T) > size_) return std::nullopt;
T value;
memcpy(&value, data_ + pos_, sizeof(T));
pos_ += sizeof(T);
return value;
}
const uint8_t *data_;
uint64_t size_;
uint64_t pos_;
};
// Function used to encode a PropertyValue into a byte stream.
std::optional<std::pair<Type, Size>> EncodePropertyValue(Writer *writer, const PropertyValue &value) {
switch (value.type()) {
case PropertyValue::Type::Null:
return {{Type::NONE, Size::INT8}};
case PropertyValue::Type::Bool: {
if (value.ValueBool()) {
return {{Type::BOOL, Size::INT64}};
} else {
return {{Type::BOOL, Size::INT8}};
}
}
case PropertyValue::Type::Int: {
auto size = writer->WriteInt(value.ValueInt());
if (!size) return std::nullopt;
return {{Type::INT, *size}};
}
case PropertyValue::Type::Double: {
auto size = writer->WriteDouble(value.ValueDouble());
if (!size) return std::nullopt;
return {{Type::DOUBLE, *size}};
}
case PropertyValue::Type::String: {
const auto &str = value.ValueString();
auto size = writer->WriteUint(str.size());
if (!size) return std::nullopt;
if (!writer->WriteBytes(str.data(), str.size())) return std::nullopt;
return {{Type::STRING, *size}};
}
case PropertyValue::Type::List: {
const auto &list = value.ValueList();
auto size = writer->WriteUint(list.size());
if (!size) return std::nullopt;
for (const auto &item : list) {
auto metadata = writer->WriteMetadata();
if (!metadata) return std::nullopt;
auto ret = EncodePropertyValue(writer, item);
if (!ret) return std::nullopt;
metadata->Set({ret->first, Size::INT8, ret->second});
}
return {{Type::LIST, *size}};
}
case PropertyValue::Type::Map: {
const auto &map = value.ValueMap();
auto size = writer->WriteUint(map.size());
if (!size) return std::nullopt;
for (const auto &item : map) {
auto metadata = writer->WriteMetadata();
if (!metadata) return std::nullopt;
auto key_size = writer->WriteUint(item.first.size());
if (!key_size) return std::nullopt;
if (!writer->WriteBytes(item.first.data(), item.first.size())) return std::nullopt;
auto ret = EncodePropertyValue(writer, item.second);
if (!ret) return std::nullopt;
metadata->Set({ret->first, *key_size, ret->second});
}
return {{Type::MAP, *size}};
}
case PropertyValue::Type::TemporalData: {
auto metadata = writer->WriteMetadata();
if (!metadata) return std::nullopt;
const auto temporal_data = value.ValueTemporalData();
auto type_size = writer->WriteUint(utils::UnderlyingCast(temporal_data.type));
if (!type_size) return std::nullopt;
auto microseconds_size = writer->WriteInt(temporal_data.microseconds);
if (!microseconds_size) return std::nullopt;
metadata->Set({Type::TEMPORAL_DATA, *type_size, *microseconds_size});
// We don't need payload size so we set it to a random value
return {{Type::TEMPORAL_DATA, Size::INT8}};
}
}
}
namespace {
std::optional<TemporalData> DecodeTemporalData(Reader &reader) {
auto metadata = reader.ReadMetadata();
if (!metadata || metadata->type != Type::TEMPORAL_DATA) return std::nullopt;
auto type_value = reader.ReadUint(metadata->id_size);
if (!type_value) return std::nullopt;
auto microseconds_value = reader.ReadInt(metadata->payload_size);
if (!microseconds_value) return std::nullopt;
return TemporalData{static_cast<TemporalType>(*type_value), *microseconds_value};
}
} // namespace
// Function used to decode a PropertyValue from a byte stream.
//
// @sa ComparePropertyValue
[[nodiscard]] bool DecodePropertyValue(Reader *reader, Type type, Size payload_size, PropertyValue &value) {
switch (type) {
case Type::EMPTY: {
return false;
}
case Type::NONE: {
value = PropertyValue();
return true;
}
case Type::BOOL: {
if (payload_size == Size::INT64) {
value = PropertyValue(true);
} else {
value = PropertyValue(false);
}
return true;
}
case Type::INT: {
auto int_v = reader->ReadInt(payload_size);
if (!int_v) return false;
value = PropertyValue(*int_v);
return true;
}
case Type::DOUBLE: {
auto double_v = reader->ReadDouble(payload_size);
if (!double_v) return false;
value = PropertyValue(*double_v);
return true;
}
case Type::STRING: {
auto size = reader->ReadUint(payload_size);
if (!size) return false;
std::string str_v(*size, '\0');
if (!reader->ReadBytes(str_v.data(), *size)) return false;
value = PropertyValue(std::move(str_v));
return true;
}
case Type::LIST: {
auto size = reader->ReadUint(payload_size);
if (!size) return false;
std::vector<PropertyValue> list;
list.reserve(*size);
for (uint64_t i = 0; i < *size; ++i) {
auto metadata = reader->ReadMetadata();
if (!metadata) return false;
PropertyValue item;
if (!DecodePropertyValue(reader, metadata->type, metadata->payload_size, item)) return false;
list.emplace_back(std::move(item));
}
value = PropertyValue(std::move(list));
return true;
}
case Type::MAP: {
auto size = reader->ReadUint(payload_size);
if (!size) return false;
std::map<std::string, PropertyValue> map;
for (uint64_t i = 0; i < *size; ++i) {
auto metadata = reader->ReadMetadata();
if (!metadata) return false;
auto key_size = reader->ReadUint(metadata->id_size);
if (!key_size) return false;
std::string key(*key_size, '\0');
if (!reader->ReadBytes(key.data(), *key_size)) return false;
PropertyValue item;
if (!DecodePropertyValue(reader, metadata->type, metadata->payload_size, item)) return false;
map.emplace(std::move(key), std::move(item));
}
value = PropertyValue(std::move(map));
return true;
}
case Type::TEMPORAL_DATA: {
const auto maybe_temporal_data = DecodeTemporalData(*reader);
if (!maybe_temporal_data) return false;
value = PropertyValue(*maybe_temporal_data);
return true;
}
}
}
// Function used to skip a PropertyValue from a byte stream.
//
// @sa ComparePropertyValue
[[nodiscard]] bool SkipPropertyValue(Reader *reader, Type type, Size payload_size) {
switch (type) {
case Type::EMPTY: {
return false;
}
case Type::NONE:
case Type::BOOL: {
return true;
}
case Type::INT: {
return reader->ReadInt(payload_size).has_value();
}
case Type::DOUBLE: {
return reader->ReadDouble(payload_size).has_value();
}
case Type::STRING: {
auto size = reader->ReadUint(payload_size);
if (!size) return false;
if (!reader->SkipBytes(*size)) return false;
return true;
}
case Type::LIST: {
auto const size = reader->ReadUint(payload_size);
if (!size) return false;
auto size_val = *size;
for (uint64_t i = 0; i != size_val; ++i) {
auto metadata = reader->ReadMetadata();
if (!metadata) return false;
if (!SkipPropertyValue(reader, metadata->type, metadata->payload_size)) return false;
}
return true;
}
case Type::MAP: {
auto const size = reader->ReadUint(payload_size);
if (!size) return false;
auto size_val = *size;
for (uint64_t i = 0; i != size_val; ++i) {
auto metadata = reader->ReadMetadata();
if (!metadata) return false;
auto key_size = reader->ReadUint(metadata->id_size);
if (!key_size) return false;
if (!reader->SkipBytes(*key_size)) return false;
if (!SkipPropertyValue(reader, metadata->type, metadata->payload_size)) return false;
}
return true;
}
case Type::TEMPORAL_DATA: {
return DecodeTemporalData(*reader).has_value();
}
}
}
// 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;
}
case Type::TEMPORAL_DATA: {
if (!value.IsTemporalData()) return false;
const auto maybe_temporal_data = DecodeTemporalData(*reader);
if (!maybe_temporal_data) {
return false;
}
return *maybe_temporal_data == value.ValueTemporalData();
}
}
}
// Function used to encode a property (PropertyId, PropertyValue) into a byte
// stream.
bool EncodeProperty(Writer *writer, PropertyId property, const PropertyValue &value) {
auto metadata = writer->WriteMetadata();
if (!metadata) return false;
auto id_size = writer->WriteUint(property.AsUint());
if (!id_size) return false;
auto type_property_size = EncodePropertyValue(writer, value);
if (!type_property_size) return false;
metadata->Set({type_property_size->first, *id_size, type_property_size->second});
return true;
}
// Enum used to return status from the `DecodeExpectedProperty` function.
enum class ExpectedPropertyStatus {
MISSING_DATA,
SMALLER,
EQUAL,
GREATER,
};
// Function used to decode a property (PropertyId, PropertyValue) from a byte
// stream. The `expected_property` provides another hint whether the property
// should be decoded or skipped.
//
// @return MISSING_DATA when there is not enough data in the buffer to decode
// the property
// @return SMALLER when the property that was currently read has a smaller
// property ID than the expected property; the value isn't
// loaded in this case
// @return EQUAL when the property that was currently read has an ID equal to
// the expected property ID; the value is loaded in this case
// @return GREATER when the property that was currenly read has a greater
// property ID than the expected property; the value isn't
// loaded in this case
//
// @sa DecodeAnyProperty
// @sa CompareExpectedProperty
[[nodiscard]] ExpectedPropertyStatus DecodeExpectedProperty(Reader *reader, PropertyId expected_property,
PropertyValue &value) {
auto metadata = reader->ReadMetadata();
if (!metadata) return ExpectedPropertyStatus::MISSING_DATA;
auto property_id = reader->ReadUint(metadata->id_size);
if (!property_id) return ExpectedPropertyStatus::MISSING_DATA;
if (*property_id == expected_property.AsUint()) {
if (!DecodePropertyValue(reader, metadata->type, metadata->payload_size, value))
return ExpectedPropertyStatus::MISSING_DATA;
return ExpectedPropertyStatus::EQUAL;
}
// Don't load the value if this isn't the expected property.
if (!SkipPropertyValue(reader, metadata->type, metadata->payload_size)) return ExpectedPropertyStatus::MISSING_DATA;
return (*property_id < expected_property.AsUint()) ? ExpectedPropertyStatus::SMALLER
: ExpectedPropertyStatus::GREATER;
}
// Function used to check a property exists (PropertyId) from a byte stream.
// It will skip the encoded PropertyValue.
//
// @return MISSING_DATA when there is not enough data in the buffer to decode
// the property
// @return SMALLER when the property that was currently read has a smaller
// property ID than the expected property; the value isn't
// loaded in this case
// @return EQUAL when the property that was currently read has an ID equal to
// the expected property ID; the value is loaded in this case
// @return GREATER when the property that was currenly read has a greater
// property ID than the expected property; the value isn't
// loaded in this case
//
// @sa DecodeAnyProperty
// @sa CompareExpectedProperty
[[nodiscard]] ExpectedPropertyStatus HasExpectedProperty(Reader *reader, PropertyId expected_property) {
auto metadata = reader->ReadMetadata();
if (!metadata) return ExpectedPropertyStatus::MISSING_DATA;
auto property_id = reader->ReadUint(metadata->id_size);
if (!property_id) return ExpectedPropertyStatus::MISSING_DATA;
if (!SkipPropertyValue(reader, metadata->type, metadata->payload_size)) return ExpectedPropertyStatus::MISSING_DATA;
if (*property_id < expected_property.AsUint()) {
return ExpectedPropertyStatus::SMALLER;
} else if (*property_id == expected_property.AsUint()) {
return ExpectedPropertyStatus::EQUAL;
} else {
return ExpectedPropertyStatus::GREATER;
}
}
// Function used to decode a property (PropertyId, PropertyValue) from a byte
// stream.
//
// @sa DecodeExpectedProperty
// @sa CompareExpectedProperty
[[nodiscard]] std::optional<PropertyId> DecodeAnyProperty(Reader *reader, PropertyValue &value) {
auto metadata = reader->ReadMetadata();
if (!metadata) return std::nullopt;
auto property_id = reader->ReadUint(metadata->id_size);
if (!property_id) return std::nullopt;
if (!DecodePropertyValue(reader, metadata->type, metadata->payload_size, value)) return std::nullopt;
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,
// the `value` won't be updated.
//
// @sa FindSpecificPropertyAndBufferInfo
[[nodiscard]] ExpectedPropertyStatus FindSpecificProperty(Reader *reader, PropertyId property, PropertyValue &value) {
while (true) {
auto ret = DecodeExpectedProperty(reader, property, value);
// Because the properties are sorted in the buffer, we only need to
// continue searching for the property while this function returns a
// `SMALLER` value indicating that the ID of the found property is smaller
// than the seeked ID. All other return values (`MISSING_DATA`, `EQUAL` and
// `GREATER`) terminate the search.
if (ret != ExpectedPropertyStatus::SMALLER) {
return ret;
}
}
}
// Function used to find if property is set. It relies on the fact that the properties
// are sorted (by ID) in the buffer.
//
// @sa FindSpecificPropertyAndBufferInfo
[[nodiscard]] ExpectedPropertyStatus ExistsSpecificProperty(Reader *reader, PropertyId property) {
while (true) {
auto ret = HasExpectedProperty(reader, property);
// Because the properties are sorted in the buffer, we only need to
// continue searching for the property while this function returns a
// `SMALLER` value indicating that the ID of the found property is smaller
// than the seeked ID. All other return values (`MISSING_DATA`, `EQUAL` and
// `GREATER`) terminate the search.
if (ret != ExpectedPropertyStatus::SMALLER) {
return ret;
}
}
}
// Struct used to return info about the property position and buffer size.
struct SpecificPropertyAndBufferInfo {
uint64_t property_begin;
uint64_t property_end;
uint64_t property_size;
uint64_t all_begin;
uint64_t all_end;
uint64_t all_size;
};
// Function used to find the position where the property should be in the data
// buffer. It keeps the properties in the buffer sorted by `PropertyId` and
// returns the positions in the buffer where the seeked property starts and
// ends. It also returns the positions where all of the properties start and
// end. Also, sizes are calculated.
// If the function doesn't find the property, the `property_size` will be `0`
// and `property_begin` will be equal to `property_end`. Positions and size of
// all properties is always calculated (even if the specific property isn't
// found).
//
// @sa FindSpecificProperty
SpecificPropertyAndBufferInfo FindSpecificPropertyAndBufferInfo(Reader *reader, PropertyId property) {
uint64_t property_begin = reader->GetPosition();
uint64_t property_end = reader->GetPosition();
uint64_t all_begin = reader->GetPosition();
uint64_t all_end = reader->GetPosition();
while (true) {
auto ret = HasExpectedProperty(reader, property);
if (ret == ExpectedPropertyStatus::MISSING_DATA) {
break;
}
if (ret == ExpectedPropertyStatus::SMALLER) {
property_begin = reader->GetPosition();
property_end = reader->GetPosition();
} else if (ret == ExpectedPropertyStatus::EQUAL) {
property_end = reader->GetPosition();
}
all_end = reader->GetPosition();
}
return {property_begin, property_end, property_end - property_begin, all_begin, all_end, all_end - all_begin};
}
// All data buffers will be allocated to a power of 8 size.
uint64_t ToPowerOf8(uint64_t size) {
uint64_t mod = size % 8;
if (mod == 0) return size;
return size - mod + 8;
}
// The `PropertyStore` also uses a small buffer optimization in it. If the data
// fits into the size of the internally stored pointer and size, then the
// pointer and size are used as a in-place buffer. In order to be able to do
// this we store a `union` of the two sets of data. Because the storage is a
// `union`, only one set of information (pointer+size or buffer) can be used at
// any time. The buffer perfectly overlaps with the memory locations of the
// pointer+size. This is illustrated in the following diagram:
//
// Memory (hex):
// 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
// |---------------------| -> size
// |---------------------| -> data
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -> buffer_ (positions)
//
// When we are using the pointer+size we know that the size must
// be a multiple of 8 (because we always allocate a buffer whose size is a
// multiple of 8). That means that the lower 3 bits of the `size` field must be
// zero when the data is used as a pointer+size.
//
// Because this architecture is little-endian, we know that `buffer_[0]` will be
// aligned with the lowest byte of the `size` field. When we use the inline
// `buffer_` we write `kUseLocalBuffer` (which is exactly 1) to `buffer_[0]`
// which will make the `size` read (independent of the other values in the
// buffer) always not be a multiple of 8. We use that fact to distinguish which
// of the two sets of data is currently active. Because the first byte of the
// buffer is used to distinguish which of the two sets of data is used, we can
// only use the leftover 15 bytes for raw data storage.
const uint8_t kUseLocalBuffer = 0x01;
// Helper functions used to retrieve/store `size` and `data` from/into the
// `buffer_`.
std::pair<uint64_t, uint8_t *> GetSizeData(const uint8_t *buffer) {
uint64_t size;
uint8_t *data;
memcpy(&size, buffer, sizeof(uint64_t));
memcpy(&data, buffer + sizeof(uint64_t), sizeof(uint8_t *));
return {size, data};
}
void SetSizeData(uint8_t *buffer, uint64_t size, uint8_t *data) {
memcpy(buffer, &size, sizeof(uint64_t));
memcpy(buffer + sizeof(uint64_t), &data, sizeof(uint8_t *));
}
} // namespace
PropertyStore::PropertyStore() { memset(buffer_, 0, sizeof(buffer_)); }
PropertyStore::PropertyStore(PropertyStore &&other) noexcept {
memcpy(buffer_, other.buffer_, sizeof(buffer_));
memset(other.buffer_, 0, sizeof(other.buffer_));
}
PropertyStore &PropertyStore::operator=(PropertyStore &&other) noexcept {
uint64_t size;
uint8_t *data;
std::tie(size, data) = GetSizeData(buffer_);
if (size % 8 == 0) {
// We are storing the data in an external buffer.
delete[] data;
}
memcpy(buffer_, other.buffer_, sizeof(buffer_));
memset(other.buffer_, 0, sizeof(other.buffer_));
return *this;
}
PropertyStore::~PropertyStore() {
uint64_t size;
uint8_t *data;
std::tie(size, data) = GetSizeData(buffer_);
if (size % 8 == 0) {
// We are storing the data in an external buffer.
delete[] data;
}
}
PropertyValue PropertyStore::GetProperty(PropertyId property) 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);
PropertyValue value;
if (FindSpecificProperty(&reader, property, value) != ExpectedPropertyStatus::EQUAL) return {};
return value;
}
bool PropertyStore::HasProperty(PropertyId property) 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);
return ExistsSpecificProperty(&reader, property) == ExpectedPropertyStatus::EQUAL;
}
/// TODO: andi write a unit test for it
bool PropertyStore::HasAllProperties(const std::set<PropertyId> &properties) const {
return std::all_of(properties.begin(), properties.end(), [this](const auto &prop) { return HasProperty(prop); });
}
/// TODO: andi write a unit test for it
bool PropertyStore::HasAllPropertyValues(const std::vector<PropertyValue> &property_values) const {
/// TODO: andi extract this into a private method
auto property_map = Properties();
std::vector<PropertyValue> all_property_values;
transform(property_map.begin(), property_map.end(), back_inserter(all_property_values),
[](const auto &kv_entry) { return kv_entry.second; });
return std::all_of(
property_values.begin(), property_values.end(), [&all_property_values](const PropertyValue &value) {
return std::find(all_property_values.begin(), all_property_values.end(), value) != all_property_values.end();
});
}
std::optional<std::vector<PropertyValue>> PropertyStore::ExtractPropertyValues(
const std::set<PropertyId> &properties) const {
std::vector<PropertyValue> value_array;
value_array.reserve(properties.size());
for (const auto &prop : properties) {
auto value = GetProperty(prop);
if (value.IsNull()) {
return std::nullopt;
}
value_array.emplace_back(std::move(value));
}
return value_array;
}
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<PropertyId, PropertyValue> PropertyStore::Properties() 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);
std::map<PropertyId, PropertyValue> props;
while (true) {
PropertyValue value;
auto prop = DecodeAnyProperty(&reader, value);
if (!prop) break;
props.emplace(*prop, std::move(value));
}
return props;
}
bool PropertyStore::SetProperty(PropertyId property, const PropertyValue &value) {
uint64_t property_size = 0;
if (!value.IsNull()) {
Writer writer;
EncodeProperty(&writer, property, value);
property_size = writer.Written();
}
bool in_local_buffer = false;
uint64_t size;
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];
in_local_buffer = true;
}
bool existed = false;
if (!size) {
if (!value.IsNull()) {
// We don't have a data buffer. Allocate a new one.
auto property_size_to_power_of_8 = ToPowerOf8(property_size);
if (property_size <= sizeof(buffer_) - 1) {
// Use the local buffer.
buffer_[0] = kUseLocalBuffer;
size = sizeof(buffer_) - 1;
data = &buffer_[1];
in_local_buffer = true;
} else {
// Allocate a new external buffer.
auto *alloc_data = new uint8_t[property_size_to_power_of_8];
auto alloc_size = property_size_to_power_of_8;
SetSizeData(buffer_, alloc_size, alloc_data);
size = alloc_size;
data = alloc_data;
in_local_buffer = false;
}
// Encode the property into the data buffer.
Writer writer(data, size);
MG_ASSERT(EncodeProperty(&writer, property, value), "Invalid database state!");
auto metadata = writer.WriteMetadata();
if (metadata) {
// If there is any space left in the buffer we add a tombstone to
// indicate that there are no more properties to be decoded.
metadata->Set({Type::EMPTY});
}
} else {
// We don't have to do anything. We don't have a buffer and we are trying
// to set a property to `Null` (we are trying to remove the property).
}
} else {
Reader reader(data, size);
auto info = FindSpecificPropertyAndBufferInfo(&reader, property);
existed = info.property_size != 0;
auto new_size = info.all_size - info.property_size + property_size;
auto new_size_to_power_of_8 = ToPowerOf8(new_size);
if (new_size_to_power_of_8 == 0) {
// We don't have any data to encode anymore.
if (!in_local_buffer) delete[] data;
SetSizeData(buffer_, 0, nullptr);
data = nullptr;
size = 0;
} else if (new_size_to_power_of_8 > size || new_size_to_power_of_8 <= size * 2 / 3) {
// We need to enlarge/shrink the buffer.
bool current_in_local_buffer = false;
uint8_t *current_data = nullptr;
uint64_t current_size = 0;
if (new_size <= sizeof(buffer_) - 1) {
// Use the local buffer.
buffer_[0] = kUseLocalBuffer;
current_size = sizeof(buffer_) - 1;
current_data = &buffer_[1];
current_in_local_buffer = true;
} else {
// Allocate a new external buffer.
current_data = new uint8_t[new_size_to_power_of_8];
current_size = new_size_to_power_of_8;
current_in_local_buffer = false;
}
// Copy everything before the property to the new buffer.
memmove(current_data, data, info.property_begin);
// Copy everything after the property to the new buffer.
memmove(current_data + info.property_begin + property_size, data + info.property_end,
info.all_end - info.property_end);
// Free the old buffer.
if (!in_local_buffer) delete[] data;
// Permanently remember the new buffer.
if (!current_in_local_buffer) {
SetSizeData(buffer_, current_size, current_data);
}
// Set the proxy variables.
data = current_data;
size = current_size;
in_local_buffer = current_in_local_buffer;
} else if (property_size != info.property_size) {
// We can keep the data in the same buffer, but the new property is
// larger/smaller than the old property. We need to move the following
// properties to the right/left.
memmove(data + info.property_begin + property_size, data + info.property_end, info.all_end - info.property_end);
}
if (!value.IsNull()) {
// We need to encode the new value.
Writer writer(data + info.property_begin, property_size);
MG_ASSERT(EncodeProperty(&writer, property, value), "Invalid database state!");
}
// We need to recreate the tombstone (if possible).
Writer writer(data + new_size, size - new_size);
auto metadata = writer.WriteMetadata();
if (metadata) {
metadata->Set({Type::EMPTY});
}
}
return !existed;
}
template <typename TContainer>
bool PropertyStore::DoInitProperties(const TContainer &properties) {
uint64_t size = 0;
uint8_t *data = nullptr;
std::tie(size, data) = GetSizeData(buffer_);
if (size != 0) {
return false;
}
uint64_t property_size = 0;
{
Writer writer;
for (const auto &[property, value] : properties) {
if (value.IsNull()) {
continue;
}
EncodeProperty(&writer, property, value);
property_size = writer.Written();
}
}
auto property_size_to_power_of_8 = ToPowerOf8(property_size);
if (property_size <= sizeof(buffer_) - 1) {
// Use the local buffer.
buffer_[0] = kUseLocalBuffer;
size = sizeof(buffer_) - 1;
data = &buffer_[1];
} else {
// Allocate a new external buffer.
auto *alloc_data = new uint8_t[property_size_to_power_of_8];
auto alloc_size = property_size_to_power_of_8;
SetSizeData(buffer_, alloc_size, alloc_data);
size = alloc_size;
data = alloc_data;
}
// Encode the property into the data buffer.
Writer writer(data, size);
for (const auto &[property, value] : properties) {
if (value.IsNull()) {
continue;
}
MG_ASSERT(EncodeProperty(&writer, property, value), "Invalid database state!");
writer.Written();
}
auto metadata = writer.WriteMetadata();
if (metadata) {
// If there is any space left in the buffer we add a tombstone to
// indicate that there are no more properties to be decoded.
metadata->Set({Type::EMPTY});
}
return true;
}
std::vector<std::tuple<PropertyId, PropertyValue, PropertyValue>> PropertyStore::UpdateProperties(
std::map<PropertyId, PropertyValue> &properties) {
auto old_properties = Properties();
ClearProperties();
std::vector<std::tuple<PropertyId, PropertyValue, PropertyValue>> id_old_new_change;
id_old_new_change.reserve(properties.size() + old_properties.size());
for (const auto &[prop_id, new_value] : properties) {
if (!old_properties.contains(prop_id)) {
id_old_new_change.emplace_back(std::make_tuple(prop_id, PropertyValue(), new_value));
}
}
for (const auto &[old_key, old_value] : old_properties) {
auto [it, inserted] = properties.emplace(old_key, old_value);
if (!inserted) {
auto &new_value = it->second;
id_old_new_change.emplace_back(std::make_tuple(it->first, old_value, new_value));
}
}
MG_ASSERT(InitProperties(properties));
return id_old_new_change;
}
template bool PropertyStore::DoInitProperties<std::map<PropertyId, PropertyValue>>(
const std::map<PropertyId, PropertyValue> &);
template bool PropertyStore::DoInitProperties<std::vector<std::pair<PropertyId, PropertyValue>>>(
const std::vector<std::pair<PropertyId, PropertyValue>> &);
bool PropertyStore::InitProperties(const std::map<storage::PropertyId, storage::PropertyValue> &properties) {
return DoInitProperties(properties);
}
bool PropertyStore::InitProperties(std::vector<std::pair<storage::PropertyId, storage::PropertyValue>> properties) {
std::sort(properties.begin(), properties.end());
return DoInitProperties(properties);
}
bool PropertyStore::ClearProperties() {
bool in_local_buffer = false;
uint64_t size;
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];
in_local_buffer = true;
}
if (!size) return false;
if (!in_local_buffer) delete[] data;
SetSizeData(buffer_, 0, nullptr);
return true;
}
std::string PropertyStore::StringBuffer() const {
uint64_t size = 0;
const uint8_t *data = nullptr;
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];
}
std::string arr(size, ' ');
for (uint i = 0; i < size; ++i) {
arr[i] = static_cast<char>(data[i]);
}
return arr;
}
void PropertyStore::SetBuffer(const std::string_view buffer) {
if (buffer.empty()) {
return;
}
uint64_t size = 0;
uint8_t *data = nullptr;
size = buffer.size();
if (buffer.size() == sizeof(buffer_) - 1) { // use local buffer
buffer_[0] = kUseLocalBuffer;
data = &buffer_[1];
} else {
data = new uint8_t[size];
SetSizeData(buffer_, size, data);
}
for (uint i = 0; i < size; ++i) {
data[i] = static_cast<uint8_t>(buffer[i]);
}
}
} // namespace memgraph::storage