Support property set for unique constraints

Summary:
Before this change, unique constraints supported only pairs of label
and a single property. With this change, unique constraints can be
created for label and set of properties.
Better tests for unique constraints in general are also included in
this diff.

Reviewers: mferencevic, teon.banek

Reviewed By: mferencevic

Subscribers: pullbot

Differential Revision: https://phabricator.memgraph.io/D2653
This commit is contained in:
Tonko Sabolcec 2020-02-13 11:52:56 +01:00
parent 4669e0ae7d
commit 95c9755e13
6 changed files with 782 additions and 212 deletions

View File

@ -930,8 +930,10 @@ PreparedQuery PrepareConstraintQuery(
auto violation = res.GetError(); auto violation = res.GetError();
auto label_name = auto label_name =
interpreter_context->db->LabelToName(violation.label); interpreter_context->db->LabelToName(violation.label);
CHECK(violation.properties.size() == 1U);
auto property_name = auto property_name =
interpreter_context->db->PropertyToName(violation.property); interpreter_context->db->PropertyToName(
*violation.properties.begin());
throw QueryRuntimeException( throw QueryRuntimeException(
"Unable to create a constraint :{}({}), because an existing " "Unable to create a constraint :{}({}), because an existing "
"node violates it.", "node violates it.",
@ -1111,8 +1113,10 @@ void Interpreter::Commit() {
const auto &constraint_violation = maybe_constraint_violation.GetError(); const auto &constraint_violation = maybe_constraint_violation.GetError();
auto label_name = auto label_name =
execution_db_accessor_->LabelToName(constraint_violation.label); execution_db_accessor_->LabelToName(constraint_violation.label);
CHECK(constraint_violation.properties.size() == 1U);
auto property_name = auto property_name =
execution_db_accessor_->PropertyToName(constraint_violation.property); execution_db_accessor_->PropertyToName(
*constraint_violation.properties.begin());
execution_db_accessor_ = std::nullopt; execution_db_accessor_ = std::nullopt;
db_accessor_ = std::nullopt; db_accessor_ = std::nullopt;
throw QueryException( throw QueryException(

View File

@ -1,28 +1,58 @@
#include "storage/v2/constraints.hpp" #include "storage/v2/constraints.hpp"
#include <set> #include <algorithm>
#include <cstring>
#include <map>
#include "storage/v2/mvcc.hpp" #include "storage/v2/mvcc.hpp"
namespace storage { namespace storage {
namespace { namespace {
/// Helper function that determines position of the given `property` in the
/// sorted `property_array` using binary search. In the case that `property`
/// cannot be found, `std::nullopt` is returned.
std::optional<size_t> FindPropertyPosition(
const PropertyIdArray &property_array, PropertyId property) {
auto it =
std::lower_bound(property_array.values,
property_array.values + property_array.size, property);
if (it == property_array.values + property_array.size || *it != property) {
return std::nullopt;
}
return it - property_array.values;
}
/// Helper function for validating unique constraints on commit. Returns true if /// Helper function for validating unique constraints on commit. Returns true if
/// the last commited version of the given vertex contains the given label and /// the last committed version of the given vertex contains the given label and
/// property value. This function should be called when commit lock is active. /// set of property values. This function should be called when commit lock is
bool LastCommitedVersionHasLabelProperty(const Vertex &vertex, LabelId label, /// active.
PropertyId key, const PropertyValue &value, bool LastCommittedVersionHasLabelProperty(
const Transaction &transaction, const Vertex &vertex, LabelId label, const std::set<PropertyId> &properties,
const PropertyValueArray &value_array, const Transaction &transaction,
uint64_t commit_timestamp) { uint64_t commit_timestamp) {
CHECK(properties.size() == value_array.size) << "Invalid database state!";
PropertyIdArray property_array(properties.size());
bool current_value_equal_to_value[kUniqueConstraintsMaxProperties];
memset(current_value_equal_to_value, 0, sizeof(current_value_equal_to_value));
// Note that a guard lock isn't necessary to access vertex's data. // Note that a guard lock isn't necessary to access vertex's data.
// Any transaction that tries to write to that vertex will result in // Any transaction that tries to write to that vertex will result in
// serialization error. // serialization error.
bool deleted = vertex.deleted; bool deleted = vertex.deleted;
bool has_label = utils::Contains(vertex.labels, label); bool has_label = utils::Contains(vertex.labels, label);
bool current_value_equal_to_value = value.IsNull();
auto it = vertex.properties.find(key); size_t i = 0;
for (const auto &property : properties) {
auto it = vertex.properties.find(property);
current_value_equal_to_value[i] = value_array.values[i]->IsNull();
if (it != vertex.properties.end()) { if (it != vertex.properties.end()) {
current_value_equal_to_value = it->second == value; current_value_equal_to_value[i] = it->second == *value_array.values[i];
}
property_array.values[i] = property;
i++;
} }
for (Delta *delta = vertex.delta; delta != nullptr; for (Delta *delta = vertex.delta; delta != nullptr;
@ -34,8 +64,10 @@ bool LastCommitedVersionHasLabelProperty(const Vertex &vertex, LabelId label,
switch (delta->action) { switch (delta->action) {
case Delta::Action::SET_PROPERTY: { case Delta::Action::SET_PROPERTY: {
if (delta->property.key == key) { auto pos = FindPropertyPosition(property_array, delta->property.key);
current_value_equal_to_value = (delta->property.value == value); if (pos) {
current_value_equal_to_value[*pos] =
delta->property.value == *value_array.values[*pos];
} }
break; break;
} }
@ -71,33 +103,61 @@ bool LastCommitedVersionHasLabelProperty(const Vertex &vertex, LabelId label,
} }
} }
return !deleted && has_label && current_value_equal_to_value; for (size_t i = 0; i < properties.size(); ++i) {
if (!current_value_equal_to_value[i]) {
return false;
}
}
return !deleted && has_label;
} }
/// Helper function for unique constraint garbage collection. Returns true if /// Helper function for unique constraint garbage collection. Returns true if
/// there's a reachable version of the vertex that has the given label and /// there's a reachable version of the vertex that has the given label and
/// property value. /// property values.
bool AnyVersionHasLabelProperty(const Vertex &vertex, LabelId label, bool AnyVersionHasLabelProperty(const Vertex &vertex, LabelId label,
PropertyId key, const PropertyValue &value, const std::set<PropertyId> &properties,
const std::vector<PropertyValue> &values,
uint64_t timestamp) { uint64_t timestamp) {
CHECK(properties.size() == values.size()) << "Invalid database state!";
PropertyIdArray property_array(properties.size());
bool current_value_equal_to_value[kUniqueConstraintsMaxProperties];
memset(current_value_equal_to_value, 0, sizeof(current_value_equal_to_value));
bool has_label; bool has_label;
bool deleted; bool deleted;
bool current_value_equal_to_value = value.IsNull();
Delta *delta; Delta *delta;
{ {
std::lock_guard<utils::SpinLock> guard(vertex.lock); std::lock_guard<utils::SpinLock> guard(vertex.lock);
has_label = utils::Contains(vertex.labels, label); has_label = utils::Contains(vertex.labels, label);
auto it = vertex.properties.find(key);
if (it != vertex.properties.end()) {
current_value_equal_to_value = it->second == value;
}
deleted = vertex.deleted; deleted = vertex.deleted;
delta = vertex.delta; delta = vertex.delta;
size_t i = 0;
for (const auto &property : properties) {
auto it = vertex.properties.find(property);
current_value_equal_to_value[i] = values[i].IsNull();
if (it != vertex.properties.end()) {
current_value_equal_to_value[i] = it->second == values[i];
}
property_array.values[i] = property;
i++;
}
} }
if (!deleted && has_label && current_value_equal_to_value) { {
bool all_values_match = true;
for (size_t i = 0; i < values.size(); ++i) {
if (!current_value_equal_to_value[i]) {
all_values_match = false;
break;
}
}
if (!deleted && has_label && all_values_match) {
return true; return true;
} }
}
while (delta != nullptr) { while (delta != nullptr) {
auto ts = delta->timestamp->load(std::memory_order_acquire); auto ts = delta->timestamp->load(std::memory_order_acquire);
@ -117,11 +177,14 @@ bool AnyVersionHasLabelProperty(const Vertex &vertex, LabelId label,
has_label = false; has_label = false;
} }
break; break;
case Delta::Action::SET_PROPERTY: case Delta::Action::SET_PROPERTY: {
if (delta->property.key == key) { auto pos = FindPropertyPosition(property_array, delta->property.key);
current_value_equal_to_value = delta->property.value == value; if (pos) {
current_value_equal_to_value[*pos] =
delta->property.value == values[*pos];
} }
break; break;
}
case Delta::Action::RECREATE_OBJECT: { case Delta::Action::RECREATE_OBJECT: {
CHECK(deleted) << "Invalid database state!"; CHECK(deleted) << "Invalid database state!";
deleted = false; deleted = false;
@ -138,7 +201,15 @@ bool AnyVersionHasLabelProperty(const Vertex &vertex, LabelId label,
case Delta::Action::REMOVE_OUT_EDGE: case Delta::Action::REMOVE_OUT_EDGE:
break; break;
} }
if (!deleted && has_label && current_value_equal_to_value) {
bool all_values_match = true;
for (size_t i = 0; i < values.size(); ++i) {
if (!current_value_equal_to_value[i]) {
all_values_match = false;
break;
}
}
if (!deleted && has_label && all_values_match) {
return true; return true;
} }
delta = delta->next.load(std::memory_order_acquire); delta = delta->next.load(std::memory_order_acquire);
@ -146,19 +217,94 @@ bool AnyVersionHasLabelProperty(const Vertex &vertex, LabelId label,
return false; return false;
} }
bool operator<(const PropertyValueArray &lhs,
const std::vector<PropertyValue> &rhs) {
size_t n = std::min(lhs.size, rhs.size());
for (size_t i = 0; i < n; ++i) {
if (*lhs.values[i] < rhs[i]) {
return true;
}
if (rhs[i] < *lhs.values[i]) {
return false;
}
}
return lhs.size < rhs.size();
}
bool operator<(const std::vector<PropertyValue> &lhs,
const PropertyValueArray &rhs) {
size_t n = std::min(lhs.size(), rhs.size);
for (size_t i = 0; i < n; ++i) {
if (lhs[i] < *rhs.values[i]) {
return true;
}
if (*rhs.values[i] < lhs[i]) {
return false;
}
}
return lhs.size() < rhs.size;
}
bool operator==(const std::vector<PropertyValue> &lhs,
const PropertyValueArray &rhs) {
if (lhs.size() != rhs.size) {
return false;
}
for (size_t i = 0; i < rhs.size; ++i) {
if (lhs[i] == *rhs.values[i]) {
continue;
}
return false;
}
return true;
}
/// Helper function that, given the set of `properties`, extracts corresponding
/// property values from the `vertex`. `PropertyValueArray` is returned instead
/// of `std::vector` to avoid `std::bad_alloc` exception.
std::optional<PropertyValueArray> ExtractPropertyValues(
const Vertex &vertex, const std::set<PropertyId> &properties) {
PropertyValueArray value_array(properties.size());
size_t i = 0;
for (const auto &prop : properties) {
auto it = vertex.properties.find(prop);
if (it == vertex.properties.end() || it->second.IsNull()) {
return std::nullopt;
}
value_array.values[i++] = &it->second;
}
return value_array;
}
/// Helper function that converts optional property value array to optional
/// `std::vector` of property values.
/// @throw std::bad_alloc
std::optional<std::vector<PropertyValue>> OptionalPropertyValueArrayToVector(
const std::optional<PropertyValueArray> &value_array) {
if (!value_array) {
return std::nullopt;
}
std::vector<PropertyValue> values;
values.reserve(value_array->size);
for (size_t i = 0; i < value_array->size; ++i) {
values.push_back(*value_array->values[i]);
}
return std::move(values);
}
} // namespace } // namespace
bool operator==(const ConstraintViolation &lhs, bool operator==(const ConstraintViolation &lhs,
const ConstraintViolation &rhs) { const ConstraintViolation &rhs) {
return lhs.type == rhs.type && lhs.label == rhs.label && return lhs.type == rhs.type && lhs.label == rhs.label &&
lhs.property == rhs.property; lhs.properties == rhs.properties;
} }
bool UniqueConstraints::Entry::operator<(const Entry &rhs) { bool UniqueConstraints::Entry::operator<(const Entry &rhs) {
if (value < rhs.value) { if (values < rhs.values) {
return true; return true;
} }
if (rhs.value < value) { if (rhs.values < values) {
return false; return false;
} }
return std::make_tuple(vertex, timestamp) < return std::make_tuple(vertex, timestamp) <
@ -166,42 +312,59 @@ bool UniqueConstraints::Entry::operator<(const Entry &rhs) {
} }
bool UniqueConstraints::Entry::operator==(const Entry &rhs) { bool UniqueConstraints::Entry::operator==(const Entry &rhs) {
return value == rhs.value && vertex == rhs.vertex && return values == rhs.values && vertex == rhs.vertex &&
timestamp == rhs.timestamp; timestamp == rhs.timestamp;
} }
bool UniqueConstraints::Entry::operator<(const PropertyValue &rhs) { bool UniqueConstraints::Entry::operator<(
return value < rhs; const std::vector<PropertyValue> &rhs) {
return values < rhs;
} }
bool UniqueConstraints::Entry::operator==(const PropertyValue &rhs) { bool UniqueConstraints::Entry::operator==(
return value == rhs; const std::vector<PropertyValue> &rhs) {
return values == rhs;
}
bool UniqueConstraints::Entry::operator<(const PropertyValueArray &rhs) {
return values < rhs;
}
bool UniqueConstraints::Entry::operator==(const PropertyValueArray &rhs) {
return values == rhs;
} }
void UniqueConstraints::UpdateBeforeCommit(const Vertex *vertex, void UniqueConstraints::UpdateBeforeCommit(const Vertex *vertex,
const Transaction &tx) { const Transaction &tx) {
for (auto &[label_prop, storage] : constraints_) { for (auto &[label_props, storage] : constraints_) {
if (!utils::Contains(vertex->labels, label_prop.first)) { if (!utils::Contains(vertex->labels, label_props.first)) {
continue; continue;
} }
auto it = vertex->properties.find(label_prop.second); auto values = OptionalPropertyValueArrayToVector(
if (it != vertex->properties.end() && !it->second.IsNull()) { ExtractPropertyValues(*vertex, label_props.second));
if (values) {
auto acc = storage.access(); auto acc = storage.access();
acc.insert(Entry{it->second, vertex, tx.start_timestamp}); acc.insert(Entry{std::move(*values), vertex, tx.start_timestamp});
} }
} }
} }
utils::BasicResult<ConstraintViolation, bool> UniqueConstraints::CreateConstraint( utils::BasicResult<ConstraintViolation, UniqueConstraints::CreationStatus>
LabelId label, PropertyId property, UniqueConstraints::CreateConstraint(
LabelId label, const std::set<PropertyId> &properties,
utils::SkipList<Vertex>::Accessor vertices) { utils::SkipList<Vertex>::Accessor vertices) {
if (properties.empty() ||
properties.size() > kUniqueConstraintsMaxProperties) {
return CreationStatus::INVALID_PROPERTIES_SIZE;
}
auto [constraint, emplaced] = constraints_.emplace( auto [constraint, emplaced] = constraints_.emplace(
std::piecewise_construct, std::piecewise_construct, std::forward_as_tuple(label, properties),
std::forward_as_tuple(label, property),
std::forward_as_tuple()); std::forward_as_tuple());
if (!emplaced) { if (!emplaced) {
// Constraint already exists. // Constraint already exists.
return false; return CreationStatus::ALREADY_EXISTS;
} }
bool violation_found = false; bool violation_found = false;
@ -213,21 +376,21 @@ utils::BasicResult<ConstraintViolation, bool> UniqueConstraints::CreateConstrain
if (vertex.deleted || !utils::Contains(vertex.labels, label)) { if (vertex.deleted || !utils::Contains(vertex.labels, label)) {
continue; continue;
} }
auto property_it = vertex.properties.find(property); auto values = OptionalPropertyValueArrayToVector(
if (property_it == vertex.properties.end()) { ExtractPropertyValues(vertex, properties));
if (!values) {
continue; continue;
} }
const PropertyValue &value = property_it->second;
// Check whether there already is a vertex with the same value for the given // Check whether there already is a vertex with the same values for the
// label and property. // given label and property.
auto it = acc.find_equal_or_greater(value); auto it = acc.find_equal_or_greater(*values);
if (it != acc.end() && it->value == value) { if (it != acc.end() && it->values == *values) {
violation_found = true; violation_found = true;
break; break;
} }
acc.insert(Entry{value, &vertex, 0}); acc.insert(Entry{std::move(*values), &vertex, 0});
} }
} }
@ -236,9 +399,9 @@ utils::BasicResult<ConstraintViolation, bool> UniqueConstraints::CreateConstrain
// be removed. // be removed.
constraints_.erase(constraint); constraints_.erase(constraint);
return ConstraintViolation{ConstraintViolation::Type::UNIQUE, label, return ConstraintViolation{ConstraintViolation::Type::UNIQUE, label,
property}; properties};
} }
return true; return CreationStatus::SUCCESS;
} }
std::optional<ConstraintViolation> UniqueConstraints::Validate( std::optional<ConstraintViolation> UniqueConstraints::Validate(
@ -247,53 +410,52 @@ std::optional<ConstraintViolation> UniqueConstraints::Validate(
if (vertex.deleted) { if (vertex.deleted) {
return std::nullopt; return std::nullopt;
} }
for (const auto &[label_prop, storage] : constraints_) { for (const auto &[label_props, storage] : constraints_) {
const auto &label = label_prop.first; const auto &label = label_props.first;
const auto &property = label_prop.second; const auto &properties = label_props.second;
auto property_it = vertex.properties.find(property); if (!utils::Contains(vertex.labels, label)) {
if (!utils::Contains(vertex.labels, label) ||
property_it == vertex.properties.end()) {
// We are only interested in constraints relevant to the current vertex,
// i.e. having (label, property) included in the given vertex.
continue; continue;
} }
const auto &value = property_it->second; auto value_array = ExtractPropertyValues(vertex, properties);
if (!value_array) {
continue;
}
auto acc = storage.access(); auto acc = storage.access();
auto it = acc.find_equal_or_greater(value); auto it = acc.find_equal_or_greater(*value_array);
for (; it != acc.end(); ++it) { for (; it != acc.end(); ++it) {
if (value < it->value) { if (*value_array < it->values) {
break; break;
} }
// The `vertex` that is going to be commited violates a unique constraint // The `vertex` that is going to be committed violates a unique constraint
// if it's different than a vertex indexed in the list of constraints and // if it's different than a vertex indexed in the list of constraints and
// has the same label and property value as the last commited version of // has the same label and property value as the last committed version of
// the vertex from the list. // the vertex from the list.
if (&vertex != it->vertex && if (&vertex != it->vertex && LastCommittedVersionHasLabelProperty(
LastCommitedVersionHasLabelProperty(*it->vertex, label, property, *it->vertex, label, properties,
value, tx, commit_timestamp)) { *value_array, tx, commit_timestamp)) {
return ConstraintViolation{ConstraintViolation::Type::UNIQUE, label, return ConstraintViolation{ConstraintViolation::Type::UNIQUE, label,
property}; properties};
} }
} }
} }
return std::nullopt; return std::nullopt;
} }
std::vector<std::pair<LabelId, PropertyId>> UniqueConstraints::ListConstraints() std::vector<std::pair<LabelId, std::set<PropertyId>>>
const { UniqueConstraints::ListConstraints() const {
std::vector<std::pair<LabelId, PropertyId>> ret; std::vector<std::pair<LabelId, std::set<PropertyId>>> ret;
ret.reserve(constraints_.size()); ret.reserve(constraints_.size());
for (const auto &item : constraints_) { for (const auto &[label_props, _] : constraints_) {
ret.push_back(item.first); ret.push_back(label_props);
} }
return ret; return ret;
} }
void UniqueConstraints::RemoveObsoleteEntries( void UniqueConstraints::RemoveObsoleteEntries(
uint64_t oldest_active_start_timestamp) { uint64_t oldest_active_start_timestamp) {
for (auto &[label_prop, storage] : constraints_) { for (auto &[label_props, storage] : constraints_) {
auto acc = storage.access(); auto acc = storage.access();
for (auto it = acc.begin(); it != acc.end();) { for (auto it = acc.begin(); it != acc.end();) {
auto next_it = it; auto next_it = it;
@ -305,9 +467,9 @@ void UniqueConstraints::RemoveObsoleteEntries(
} }
if ((next_it != acc.end() && it->vertex == next_it->vertex && if ((next_it != acc.end() && it->vertex == next_it->vertex &&
it->value == next_it->value) || it->values == next_it->values) ||
!AnyVersionHasLabelProperty(*it->vertex, label_prop.first, !AnyVersionHasLabelProperty(*it->vertex, label_props.first,
label_prop.second, it->value, label_props.second, it->values,
oldest_active_start_timestamp)) { oldest_active_start_timestamp)) {
acc.remove(*it); acc.remove(*it);
} }

View File

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <optional> #include <optional>
#include <set>
#include <vector> #include <vector>
#include "storage/v2/id_types.hpp" #include "storage/v2/id_types.hpp"
@ -11,6 +12,25 @@
namespace storage { namespace storage {
// NOLINTNEXTLINE(misc-definitions-in-headers)
const size_t kUniqueConstraintsMaxProperties = 32;
/// Utility class to store data in a fixed size array. The array is used
/// instead of `std::vector` to avoid `std::bad_alloc` exception where not
/// necessary.
template <class T>
struct FixedCapacityArray {
size_t size;
T values[kUniqueConstraintsMaxProperties];
explicit FixedCapacityArray(size_t array_size) : size(array_size) {
CHECK(size <= kUniqueConstraintsMaxProperties) << "Invalid array size!";
}
};
using PropertyValueArray = FixedCapacityArray<const PropertyValue *>;
using PropertyIdArray = FixedCapacityArray<PropertyId>;
struct ConstraintViolation { struct ConstraintViolation {
enum class Type { enum class Type {
EXISTENCE, EXISTENCE,
@ -19,51 +39,65 @@ struct ConstraintViolation {
Type type; Type type;
LabelId label; LabelId label;
PropertyId property;
// While multiple properties are supported by unique constraints, the
// `properties` set will always have exactly one element in the case of
// existence constraint violation.
std::set<PropertyId> properties;
}; };
bool operator==(const ConstraintViolation &lhs, bool operator==(const ConstraintViolation &lhs, const ConstraintViolation &rhs);
const ConstraintViolation &rhs);
// TODO(tsabolcec): Support property sets. Unique constraints could be defined
// for pairs (label, property set). However, current implementation supports
// only pairs of label and a single property.
class UniqueConstraints { class UniqueConstraints {
private: private:
struct Entry { struct Entry {
PropertyValue value; std::vector<PropertyValue> values;
const Vertex *vertex; const Vertex *vertex;
uint64_t timestamp; uint64_t timestamp;
bool operator<(const Entry &rhs); bool operator<(const Entry &rhs);
bool operator==(const Entry &rhs); bool operator==(const Entry &rhs);
bool operator<(const PropertyValue &rhs); bool operator<(const std::vector<PropertyValue> &rhs);
bool operator==(const PropertyValue &rhs); bool operator==(const std::vector<PropertyValue> &rhs);
bool operator<(const PropertyValueArray &rhs);
bool operator==(const PropertyValueArray &rhs);
}; };
public: public:
/// Status for creation of unique constraints.
/// Note that this does not cover the case when the constraint is violated.
enum class CreationStatus {
SUCCESS,
ALREADY_EXISTS,
INVALID_PROPERTIES_SIZE,
};
/// Indexes the given vertex for relevant labels and properties. /// Indexes the given vertex for relevant labels and properties.
/// This method should be called before committing and validating vertices /// This method should be called before committing and validating vertices
/// against unique constraints. /// against unique constraints.
/// @throw std::bad_alloc /// @throw std::bad_alloc
void UpdateBeforeCommit(const Vertex *vertex, const Transaction &tx); void UpdateBeforeCommit(const Vertex *vertex, const Transaction &tx);
/// Creates unique constraint on the given `label` and `property`. /// Creates unique constraint on the given `label` and a list of `properties`.
/// Returns constraint violation if there are multiple vertices with the same /// Returns constraint violation if there are multiple vertices with the same
/// label and property values. Returns false if constraint already existed and /// label and property values. Returns `CreationStatus::ALREADY_EXISTS` if
/// true on success. /// constraint already existed, `CreationStatus::INVALID_PROPERTY_SIZE` if
/// the given list of properties is empty or the list of properties exceeds
/// the maximum allowed number of properties, and `CreationStatus::SUCCESS` on
/// success.
/// @throw std::bad_alloc /// @throw std::bad_alloc
utils::BasicResult<ConstraintViolation, bool> CreateConstraint( utils::BasicResult<ConstraintViolation, CreationStatus> CreateConstraint(
LabelId label, PropertyId property, LabelId label, const std::set<PropertyId> &properties,
utils::SkipList<Vertex>::Accessor vertices); utils::SkipList<Vertex>::Accessor vertices);
bool DropConstraint(LabelId label, PropertyId property) { bool DropConstraint(LabelId label, const std::set<PropertyId> &properties) {
return constraints_.erase({label, property}) > 0; return constraints_.erase({label, properties}) > 0;
} }
bool ConstraintExists(LabelId label, PropertyId property) const { bool ConstraintExists(LabelId label, const std::set<PropertyId> &properties) {
return constraints_.find({label, property}) != constraints_.end(); return constraints_.find({label, properties}) != constraints_.end();
} }
/// Validates the given vertex against unique constraints before committing. /// Validates the given vertex against unique constraints before committing.
@ -73,14 +107,16 @@ class UniqueConstraints {
const Transaction &tx, const Transaction &tx,
uint64_t commit_timestamp) const; uint64_t commit_timestamp) const;
std::vector<std::pair<LabelId, PropertyId>> ListConstraints() const; std::vector<std::pair<LabelId, std::set<PropertyId>>> ListConstraints() const;
/// GC method that removes outdated entries from constraints' storages.
void RemoveObsoleteEntries(uint64_t oldest_active_start_timestamp); void RemoveObsoleteEntries(uint64_t oldest_active_start_timestamp);
void Clear() { constraints_.clear(); } void Clear() { constraints_.clear(); }
private: private:
std::map<std::pair<LabelId, PropertyId>, utils::SkipList<Entry>> constraints_; std::map<std::pair<LabelId, std::set<PropertyId>>, utils::SkipList<Entry>>
constraints_;
}; };
struct Constraints { struct Constraints {
@ -89,15 +125,14 @@ struct Constraints {
}; };
/// Adds a unique constraint to `constraints`. Returns true if the constraint /// Adds a unique constraint to `constraints`. Returns true if the constraint
/// was successfuly added, false if it already exists and an /// was successfully added, false if it already exists and a
/// `ExistenceConstraintViolation` if there is an existing vertex violating the /// `ConstraintViolation` if there is an existing vertex violating the
/// constraint. /// constraint.
/// ///
/// @throw std::bad_alloc /// @throw std::bad_alloc
/// @throw std::length_error /// @throw std::length_error
inline utils::BasicResult<ConstraintViolation, bool> inline utils::BasicResult<ConstraintViolation, bool> CreateExistenceConstraint(
CreateExistenceConstraint(Constraints *constraints, LabelId label, Constraints *constraints, LabelId label, PropertyId property,
PropertyId property,
utils::SkipList<Vertex>::Accessor vertices) { utils::SkipList<Vertex>::Accessor vertices) {
if (utils::Contains(constraints->existence_constraints, if (utils::Contains(constraints->existence_constraints,
std::make_pair(label, property))) { std::make_pair(label, property))) {
@ -107,7 +142,7 @@ CreateExistenceConstraint(Constraints *constraints, LabelId label,
if (!vertex.deleted && utils::Contains(vertex.labels, label) && if (!vertex.deleted && utils::Contains(vertex.labels, label) &&
vertex.properties.find(property) == vertex.properties.end()) { vertex.properties.find(property) == vertex.properties.end()) {
return ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label, return ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label,
property}; std::set<PropertyId>{property}};
} }
} }
constraints->existence_constraints.emplace_back(label, property); constraints->existence_constraints.emplace_back(label, property);
@ -129,8 +164,8 @@ inline bool DropExistenceConstraint(Constraints *constraints, LabelId label,
} }
/// Verifies that the given vertex satisfies all existence constraints. Returns /// Verifies that the given vertex satisfies all existence constraints. Returns
/// nullopt if all checks pass, and `ExistenceConstraintViolation` describing /// `std::nullopt` if all checks pass, and `ConstraintViolation` describing the
/// the violated constraint otherwise. /// violated constraint otherwise.
[[nodiscard]] inline std::optional<ConstraintViolation> [[nodiscard]] inline std::optional<ConstraintViolation>
ValidateExistenceConstraints(const Vertex &vertex, ValidateExistenceConstraints(const Vertex &vertex,
const Constraints &constraints) { const Constraints &constraints) {
@ -138,7 +173,7 @@ ValidateExistenceConstraints(const Vertex &vertex,
if (!vertex.deleted && utils::Contains(vertex.labels, label) && if (!vertex.deleted && utils::Contains(vertex.labels, label) &&
vertex.properties.find(property) == vertex.properties.end()) { vertex.properties.find(property) == vertex.properties.end()) {
return ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label, return ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label,
property}; std::set<PropertyId>{property}};
} }
} }
return std::nullopt; return std::nullopt;

View File

@ -17,7 +17,8 @@ auto AdvanceToVisibleVertex(utils::SkipList<Vertex>::Iterator it,
Transaction *tx, View view, Indices *indices, Transaction *tx, View view, Indices *indices,
Constraints *constraints, Config::Items config) { Constraints *constraints, Config::Items config) {
while (it != end) { while (it != end) {
*vertex = VertexAccessor::Create(&*it, tx, indices, constraints, config, view); *vertex =
VertexAccessor::Create(&*it, tx, indices, constraints, config, view);
if (!*vertex) { if (!*vertex) {
++it; ++it;
continue; continue;
@ -638,8 +639,7 @@ EdgeTypeId Storage::Accessor::NameToEdgeType(const std::string &name) {
void Storage::Accessor::AdvanceCommand() { ++transaction_.command_id; } void Storage::Accessor::AdvanceCommand() { ++transaction_.command_id; }
utils::BasicResult<ConstraintViolation, void> utils::BasicResult<ConstraintViolation, void> Storage::Accessor::Commit() {
Storage::Accessor::Commit() {
CHECK(is_transaction_active_) << "The transaction is already terminated!"; CHECK(is_transaction_active_) << "The transaction is already terminated!";
CHECK(!transaction_.must_abort) << "The transaction can't be committed!"; CHECK(!transaction_.must_abort) << "The transaction can't be committed!";
@ -700,8 +700,9 @@ Storage::Accessor::Commit() {
// No need to take any locks here because we modified this vertex and no // No need to take any locks here because we modified this vertex and no
// one else can touch it until we commit. // one else can touch it until we commit.
unique_constraint_violation = storage_->constraints_.unique_constraints unique_constraint_violation =
.Validate(*prev.vertex, transaction_, commit_timestamp); storage_->constraints_.unique_constraints.Validate(
*prev.vertex, transaction_, commit_timestamp);
if (unique_constraint_violation) { if (unique_constraint_violation) {
break; break;
} }
@ -712,8 +713,8 @@ Storage::Accessor::Commit() {
// that committed transactions are sorted by the commit timestamp in the // that committed transactions are sorted by the commit timestamp in the
// WAL files. We supply the new commit timestamp to the function so that // WAL files. We supply the new commit timestamp to the function so that
// it knows what will be the final commit timestamp. The WAL must be // it knows what will be the final commit timestamp. The WAL must be
// written before actually committing the transaction (before setting the // written before actually committing the transaction (before setting
// commit timestamp) so that no other transaction can see the // the commit timestamp) so that no other transaction can see the
// modifications before they are written to disk. // modifications before they are written to disk.
storage_->durability_.AppendToWal(transaction_, commit_timestamp); storage_->durability_.AppendToWal(transaction_, commit_timestamp);
@ -728,8 +729,8 @@ Storage::Accessor::Commit() {
<< "Invalid database state!"; << "Invalid database state!";
transaction_.commit_timestamp->store(commit_timestamp, transaction_.commit_timestamp->store(commit_timestamp,
std::memory_order_release); std::memory_order_release);
// Release engine lock because we don't have to hold it anymore and // Release engine lock because we don't have to hold it anymore
// emplace back could take a long time. // and emplace back could take a long time.
engine_guard.unlock(); engine_guard.unlock();
committed_transactions.emplace_back(std::move(transaction_)); committed_transactions.emplace_back(std::move(transaction_));
}); });
@ -1056,20 +1057,20 @@ bool Storage::DropExistenceConstraint(LabelId label, PropertyId property) {
return true; return true;
} }
utils::BasicResult<ConstraintViolation, bool> utils::BasicResult<ConstraintViolation, UniqueConstraints::CreationStatus>
Storage::CreateUniqueConstraint(LabelId label, PropertyId property) { Storage::CreateUniqueConstraint(LabelId label,
const std::set<PropertyId> &properties) {
std::unique_lock<utils::RWLock> storage_guard(main_lock_); std::unique_lock<utils::RWLock> storage_guard(main_lock_);
auto ret = constraints_.unique_constraints.CreateConstraint(
label, property, vertices_.access());
if (ret.HasError() || !ret.GetValue()) return ret;
// TODO(tsabolcec): Append action to the WAL. // TODO(tsabolcec): Append action to the WAL.
return true; return constraints_.unique_constraints.CreateConstraint(label, properties,
vertices_.access());
} }
bool Storage::DropUniqueConstraint(LabelId label, PropertyId property) { bool Storage::DropUniqueConstraint(LabelId label,
const std::set<PropertyId> &properties) {
std::unique_lock<utils::RWLock> storage_guard(main_lock_); std::unique_lock<utils::RWLock> storage_guard(main_lock_);
// TODO(tsabolcec): Append action to the WAL. // TODO(tsabolcec): Append action to the WAL.
return constraints_.unique_constraints.DropConstraint(label, property); return constraints_.unique_constraints.DropConstraint(label, properties);
} }
ConstraintsInfo Storage::ListAllConstraints() const { ConstraintsInfo Storage::ListAllConstraints() const {

View File

@ -146,7 +146,7 @@ struct IndicesInfo {
/// storage. /// storage.
struct ConstraintsInfo { struct ConstraintsInfo {
std::vector<std::pair<LabelId, PropertyId>> existence; std::vector<std::pair<LabelId, PropertyId>> existence;
std::vector<std::pair<LabelId, PropertyId>> unique; std::vector<std::pair<LabelId, std::set<PropertyId>>> unique;
}; };
/// Structure used to return information about the storage. /// Structure used to return information about the storage.
@ -344,24 +344,29 @@ class Storage final {
/// ///
/// @throw std::bad_alloc /// @throw std::bad_alloc
/// @throw std::length_error /// @throw std::length_error
utils::BasicResult<ConstraintViolation, bool> utils::BasicResult<ConstraintViolation, bool> CreateExistenceConstraint(
CreateExistenceConstraint(LabelId label, PropertyId property); LabelId label, PropertyId property);
/// Removes an existence constraint. Returns true if the constraint was /// Removes an existence constraint. Returns true if the constraint was
/// removed, and false if it doesn't exist. /// removed, and false if it doesn't exist.
bool DropExistenceConstraint(LabelId label, PropertyId property); bool DropExistenceConstraint(LabelId label, PropertyId property);
/// Creates a unique constraint. Returns true if the constraint was /// Creates a unique constraint. In the case of two vertices violating the
/// successfully added, false if it already exists and a `ConstraintViolation` /// constraint, it returns `ConstraintViolation`. Otherwise returns a
/// if there are at least two vertices violating the constraint. /// `UniqueConstraints::CreationStatus` enum with the following possibilities:
/// * `SUCCESS` if the constraint was successfully created,
/// * `ALREADY_EXISTS` if the constraint already existed, or
/// * `INVALID_PROPERTIES_SIZE` if the property set is empty or exceeds
/// the limit of maximum number of properties.
/// ///
/// @throw std::bad_alloc /// @throw std::bad_alloc
utils::BasicResult<ConstraintViolation, bool> utils::BasicResult<ConstraintViolation, UniqueConstraints::CreationStatus>
CreateUniqueConstraint(LabelId label, PropertyId property); CreateUniqueConstraint(LabelId label, const std::set<PropertyId> &properties);
/// Removes a unique constraint. Returns true if the constraint was removed, /// Removes a unique constraint. Returns true if the constraint was removed,
/// and false if it doesn't exist. /// and false if it doesn't exist.
bool DropUniqueConstraint(LabelId label, PropertyId property); bool DropUniqueConstraint(LabelId label,
const std::set<PropertyId> &properties);
ConstraintsInfo ListAllConstraints() const; ConstraintsInfo ListAllConstraints() const;

View File

@ -11,12 +11,6 @@ using testing::UnorderedElementsAre;
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) // NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define ASSERT_NO_ERROR(result) ASSERT_FALSE((result).HasError()) #define ASSERT_NO_ERROR(result) ASSERT_FALSE((result).HasError())
bool operator==(const ConstraintViolation &lhs,
const ConstraintViolation &rhs) {
return lhs.type == rhs.type && lhs.label == rhs.label &&
lhs.property == rhs.property;
}
class ConstraintsTest : public testing::Test { class ConstraintsTest : public testing::Test {
protected: protected:
ConstraintsTest() ConstraintsTest()
@ -82,7 +76,7 @@ TEST_F(ConstraintsTest, ExistenceConstraintsCreateFailure1) {
ASSERT_TRUE(res.HasError()); ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(), EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label1, (ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label1,
prop1})); std::set<PropertyId>{prop1}}));
} }
{ {
auto acc = storage.Access(); auto acc = storage.Access();
@ -110,7 +104,7 @@ TEST_F(ConstraintsTest, ExistenceConstraintsCreateFailure2) {
ASSERT_TRUE(res.HasError()); ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(), EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label1, (ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label1,
prop1})); std::set<PropertyId>{prop1}}));
} }
{ {
auto acc = storage.Access(); auto acc = storage.Access();
@ -141,7 +135,7 @@ TEST_F(ConstraintsTest, ExistenceConstraintsViolationOnCommit) {
ASSERT_TRUE(res.HasError()); ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(), EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label1, (ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label1,
prop1})); std::set<PropertyId>{prop1}}));
} }
{ {
@ -162,7 +156,7 @@ TEST_F(ConstraintsTest, ExistenceConstraintsViolationOnCommit) {
ASSERT_TRUE(res.HasError()); ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(), EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label1, (ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label1,
prop1})); std::set<PropertyId>{prop1}}));
} }
{ {
@ -191,37 +185,48 @@ TEST_F(ConstraintsTest, ExistenceConstraintsViolationOnCommit) {
TEST_F(ConstraintsTest, UniqueConstraintsCreateAndDrop) { TEST_F(ConstraintsTest, UniqueConstraintsCreateAndDrop) {
EXPECT_EQ(storage.ListAllConstraints().unique.size(), 0); EXPECT_EQ(storage.ListAllConstraints().unique.size(), 0);
{ {
auto res = storage.CreateUniqueConstraint(label1, prop1); auto res = storage.CreateUniqueConstraint(label1, {prop1});
EXPECT_TRUE(res.HasValue() && res.GetValue()); EXPECT_TRUE(res.HasValue());
EXPECT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
} }
EXPECT_THAT(storage.ListAllConstraints().unique, EXPECT_THAT(storage.ListAllConstraints().unique,
UnorderedElementsAre(std::make_pair(label1, prop1))); UnorderedElementsAre(
std::make_pair(label1, std::set<PropertyId>{prop1})));
{ {
auto res = storage.CreateUniqueConstraint(label1, prop1); auto res = storage.CreateUniqueConstraint(label1, {prop1});
EXPECT_TRUE(res.HasValue() && !res.GetValue()); EXPECT_TRUE(res.HasValue());
EXPECT_EQ(res.GetValue(),
UniqueConstraints::CreationStatus::ALREADY_EXISTS);
} }
EXPECT_THAT(storage.ListAllConstraints().unique, EXPECT_THAT(storage.ListAllConstraints().unique,
UnorderedElementsAre(std::make_pair(label1, prop1))); UnorderedElementsAre(
std::make_pair(label1, std::set<PropertyId>{prop1})));
{ {
auto res = storage.CreateUniqueConstraint(label2, prop1); auto res = storage.CreateUniqueConstraint(label2, {prop1});
EXPECT_TRUE(res.HasValue() && res.GetValue()); EXPECT_TRUE(res.HasValue() &&
res.GetValue() == UniqueConstraints::CreationStatus::SUCCESS);
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
} }
EXPECT_THAT(storage.ListAllConstraints().unique, EXPECT_THAT(storage.ListAllConstraints().unique,
UnorderedElementsAre(std::make_pair(label1, prop1), UnorderedElementsAre(
std::make_pair(label2, prop1))); std::make_pair(label1, std::set<PropertyId>{prop1}),
EXPECT_TRUE(storage.DropUniqueConstraint(label1, prop1)); std::make_pair(label2, std::set<PropertyId>{prop1})));
EXPECT_FALSE(storage.DropUniqueConstraint(label1, prop1)); EXPECT_TRUE(storage.DropUniqueConstraint(label1, {prop1}));
EXPECT_FALSE(storage.DropUniqueConstraint(label1, {prop1}));
EXPECT_THAT(storage.ListAllConstraints().unique, EXPECT_THAT(storage.ListAllConstraints().unique,
UnorderedElementsAre(std::make_pair(label2, prop1))); UnorderedElementsAre(
EXPECT_TRUE(storage.DropUniqueConstraint(label2, prop1)); std::make_pair(label2, std::set<PropertyId>{prop1})));
EXPECT_FALSE(storage.DropUniqueConstraint(label2, prop2)); EXPECT_TRUE(storage.DropUniqueConstraint(label2, {prop1}));
EXPECT_FALSE(storage.DropUniqueConstraint(label2, {prop2}));
EXPECT_EQ(storage.ListAllConstraints().unique.size(), 0); EXPECT_EQ(storage.ListAllConstraints().unique.size(), 0);
{ {
auto res = storage.CreateUniqueConstraint(label2, prop1); auto res = storage.CreateUniqueConstraint(label2, {prop1});
EXPECT_TRUE(res.HasValue() && res.GetValue()); EXPECT_TRUE(res.HasValue());
EXPECT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
} }
EXPECT_THAT(storage.ListAllConstraints().unique, EXPECT_THAT(storage.ListAllConstraints().unique,
UnorderedElementsAre(std::make_pair(label2, prop1))); UnorderedElementsAre(
std::make_pair(label2, std::set<PropertyId>{prop1})));
} }
// NOLINTNEXTLINE(hicpp-special-member-functions) // NOLINTNEXTLINE(hicpp-special-member-functions)
@ -237,11 +242,11 @@ TEST_F(ConstraintsTest, UniqueConstraintsCreateFailure1) {
} }
{ {
auto res = storage.CreateUniqueConstraint(label1, prop1); auto res = storage.CreateUniqueConstraint(label1, {prop1});
ASSERT_TRUE(res.HasError()); ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(), EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1, (ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
prop1})); std::set<PropertyId>{prop1}}));
} }
{ {
@ -253,8 +258,9 @@ TEST_F(ConstraintsTest, UniqueConstraintsCreateFailure1) {
} }
{ {
auto res = storage.CreateUniqueConstraint(label1, prop1); auto res = storage.CreateUniqueConstraint(label1, {prop1});
EXPECT_TRUE(!res.HasError() && res.GetValue()); ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
} }
} }
@ -271,11 +277,11 @@ TEST_F(ConstraintsTest, UniqueConstraintsCreateFailure2) {
} }
{ {
auto res = storage.CreateUniqueConstraint(label1, prop1); auto res = storage.CreateUniqueConstraint(label1, {prop1});
ASSERT_TRUE(res.HasError()); ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(), EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1, (ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
prop1})); std::set<PropertyId>{prop1}}));
} }
{ {
@ -289,18 +295,14 @@ TEST_F(ConstraintsTest, UniqueConstraintsCreateFailure2) {
} }
{ {
auto res = storage.CreateUniqueConstraint(label1, prop1); auto res = storage.CreateUniqueConstraint(label1, {prop1});
EXPECT_TRUE(!res.HasError() && res.GetValue()); ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
} }
} }
// NOLINTNEXTLINE(hicpp-special-member-functions) // NOLINTNEXTLINE(hicpp-special-member-functions)
TEST_F(ConstraintsTest, UniqueConstraintsNoViolation1) { TEST_F(ConstraintsTest, UniqueConstraintsNoViolation1) {
{
auto res = storage.CreateUniqueConstraint(label1, prop1);
ASSERT_TRUE(res.HasValue() && res.GetValue());
}
Gid gid1; Gid gid1;
Gid gid2; Gid gid2;
{ {
@ -309,10 +311,27 @@ TEST_F(ConstraintsTest, UniqueConstraintsNoViolation1) {
auto vertex2 = acc.CreateVertex(); auto vertex2 = acc.CreateVertex();
gid1 = vertex1.Gid(); gid1 = vertex1.Gid();
gid2 = vertex2.Gid(); gid2 = vertex2.Gid();
ASSERT_NO_ERROR(vertex1.AddLabel(label1)); ASSERT_NO_ERROR(vertex1.AddLabel(label1));
ASSERT_NO_ERROR(vertex1.SetProperty(prop1, PropertyValue(1))); ASSERT_NO_ERROR(vertex1.SetProperty(prop1, PropertyValue(1)));
ASSERT_NO_ERROR(vertex2.AddLabel(label1)); ASSERT_NO_ERROR(acc.Commit());
ASSERT_NO_ERROR(vertex2.SetProperty(prop1, PropertyValue(2))); }
{
auto res = storage.CreateUniqueConstraint(label1, {prop1, prop2});
ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
}
{
auto acc = storage.Access();
auto vertex1 = acc.FindVertex(gid1, View::OLD);
auto vertex2 = acc.FindVertex(gid2, View::OLD);
ASSERT_NO_ERROR(vertex1->SetProperty(prop2, PropertyValue(2)));
ASSERT_NO_ERROR(vertex2->AddLabel(label1));
ASSERT_NO_ERROR(vertex2->SetProperty(prop1, PropertyValue(1)));
ASSERT_NO_ERROR(vertex2->SetProperty(prop2, PropertyValue(3)));
ASSERT_NO_ERROR(acc.Commit()); ASSERT_NO_ERROR(acc.Commit());
} }
@ -329,8 +348,9 @@ TEST_F(ConstraintsTest, UniqueConstraintsNoViolation1) {
// NOLINTNEXTLINE(hicpp-special-member-functions) // NOLINTNEXTLINE(hicpp-special-member-functions)
TEST_F(ConstraintsTest, UniqueConstraintsNoViolation2) { TEST_F(ConstraintsTest, UniqueConstraintsNoViolation2) {
{ {
auto res = storage.CreateUniqueConstraint(label1, prop1); auto res = storage.CreateUniqueConstraint(label1, {prop1});
ASSERT_TRUE(res.HasValue() && res.GetValue()); ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
} }
{ {
@ -358,8 +378,9 @@ TEST_F(ConstraintsTest, UniqueConstraintsNoViolation2) {
// NOLINTNEXTLINE(hicpp-special-member-functions) // NOLINTNEXTLINE(hicpp-special-member-functions)
TEST_F(ConstraintsTest, UniqueConstraintsNoViolation3) { TEST_F(ConstraintsTest, UniqueConstraintsNoViolation3) {
{ {
auto res = storage.CreateUniqueConstraint(label1, prop1); auto res = storage.CreateUniqueConstraint(label1, {prop1});
ASSERT_TRUE(res.HasValue() && res.GetValue()); ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
} }
{ {
@ -393,8 +414,9 @@ TEST_F(ConstraintsTest, UniqueConstraintsNoViolation3) {
// NOLINTNEXTLINE(hicpp-special-member-functions) // NOLINTNEXTLINE(hicpp-special-member-functions)
TEST_F(ConstraintsTest, UniqueConstraintsNoViolation4) { TEST_F(ConstraintsTest, UniqueConstraintsNoViolation4) {
{ {
auto res = storage.CreateUniqueConstraint(label1, prop1); auto res = storage.CreateUniqueConstraint(label1, {prop1});
ASSERT_TRUE(res.HasValue() && res.GetValue()); ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
} }
{ {
@ -428,8 +450,9 @@ TEST_F(ConstraintsTest, UniqueConstraintsNoViolation4) {
// NOLINTNEXTLINE(hicpp-special-member-functions) // NOLINTNEXTLINE(hicpp-special-member-functions)
TEST_F(ConstraintsTest, UniqueConstraintsViolationOnCommit1) { TEST_F(ConstraintsTest, UniqueConstraintsViolationOnCommit1) {
{ {
auto res = storage.CreateUniqueConstraint(label1, prop1); auto res = storage.CreateUniqueConstraint(label1, {prop1});
ASSERT_TRUE(res.HasValue() && res.GetValue()); ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
} }
{ {
@ -444,15 +467,16 @@ TEST_F(ConstraintsTest, UniqueConstraintsViolationOnCommit1) {
ASSERT_TRUE(res.HasError()); ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(), EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1, (ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
prop1})); std::set<PropertyId>{prop1}}));
} }
} }
// NOLINTNEXTLINE(hicpp-special-member-functions) // NOLINTNEXTLINE(hicpp-special-member-functions)
TEST_F(ConstraintsTest, UniqueConstraintsViolationOnCommit2) { TEST_F(ConstraintsTest, UniqueConstraintsViolationOnCommit2) {
{ {
auto res = storage.CreateUniqueConstraint(label1, prop1); auto res = storage.CreateUniqueConstraint(label1, {prop1});
ASSERT_TRUE(res.HasValue() && res.GetValue()); ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
} }
{ {
@ -486,15 +510,16 @@ TEST_F(ConstraintsTest, UniqueConstraintsViolationOnCommit2) {
ASSERT_TRUE(res.HasError()); ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(), EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1, (ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
prop1})); std::set<PropertyId>{prop1}}));
} }
} }
// NOLINTNEXTLINE(hicpp-special-member-functions) // NOLINTNEXTLINE(hicpp-special-member-functions)
TEST_F(ConstraintsTest, UniqueConstraintsViolationOnCommit3) { TEST_F(ConstraintsTest, UniqueConstraintsViolationOnCommit3) {
{ {
auto res = storage.CreateUniqueConstraint(label1, prop1); auto res = storage.CreateUniqueConstraint(label1, {prop1});
ASSERT_TRUE(res.HasValue() & res.GetValue()); ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
} }
{ {
@ -520,6 +545,10 @@ TEST_F(ConstraintsTest, UniqueConstraintsViolationOnCommit3) {
auto vertex3 = acc2.FindVertex(gid1, View::OLD); // vertex3 == vertex1 auto vertex3 = acc2.FindVertex(gid1, View::OLD); // vertex3 == vertex1
auto vertex4 = acc3.FindVertex(gid2, View::OLD); // vertex4 == vertex2 auto vertex4 = acc3.FindVertex(gid2, View::OLD); // vertex4 == vertex2
// Setting `prop2` shouldn't affect the remaining code.
ASSERT_NO_ERROR(vertex3->SetProperty(prop2, PropertyValue(3)));
ASSERT_NO_ERROR(vertex4->SetProperty(prop2, PropertyValue(3)));
ASSERT_NO_ERROR(vertex3->SetProperty(prop1, PropertyValue(2))); ASSERT_NO_ERROR(vertex3->SetProperty(prop1, PropertyValue(2)));
ASSERT_NO_ERROR(vertex4->SetProperty(prop1, PropertyValue(1))); ASSERT_NO_ERROR(vertex4->SetProperty(prop1, PropertyValue(1)));
@ -527,20 +556,21 @@ TEST_F(ConstraintsTest, UniqueConstraintsViolationOnCommit3) {
ASSERT_TRUE(res.HasError()); ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(), EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1, (ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
prop1})); std::set<PropertyId>{prop1}}));
res = acc3.Commit(); res = acc3.Commit();
ASSERT_TRUE(res.HasError()); ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(), EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1, (ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
prop1})); std::set<PropertyId>{prop1}}));
} }
} }
// NOLINTNEXTLINE(hicpp-special-member-functions) // NOLINTNEXTLINE(hicpp-special-member-functions)
TEST_F(ConstraintsTest, UniqueConstraintsLabelAlteration) { TEST_F(ConstraintsTest, UniqueConstraintsLabelAlteration) {
{ {
auto res = storage.CreateUniqueConstraint(label1, prop1); auto res = storage.CreateUniqueConstraint(label1, {prop1});
ASSERT_TRUE(res.HasValue() && res.GetValue()); ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
} }
Gid gid1; Gid gid1;
@ -574,7 +604,22 @@ TEST_F(ConstraintsTest, UniqueConstraintsLabelAlteration) {
ASSERT_NO_ERROR(vertex1->AddLabel(label1)); ASSERT_NO_ERROR(vertex1->AddLabel(label1));
ASSERT_NO_ERROR(vertex2->RemoveLabel(label1)); ASSERT_NO_ERROR(vertex2->RemoveLabel(label1));
// Reapplying labels shouldn't affect the remaining code.
ASSERT_NO_ERROR(vertex1->RemoveLabel(label1));
ASSERT_NO_ERROR(vertex2->AddLabel(label1));
ASSERT_NO_ERROR(vertex1->AddLabel(label1));
ASSERT_NO_ERROR(vertex2->RemoveLabel(label1));
ASSERT_NO_ERROR(vertex1->RemoveLabel(label2));
// Commit the second transaction.
ASSERT_NO_ERROR(acc2.Commit()); ASSERT_NO_ERROR(acc2.Commit());
// Reapplying labels after first commit shouldn't affect the remaining code.
ASSERT_NO_ERROR(vertex1->RemoveLabel(label1));
ASSERT_NO_ERROR(vertex1->AddLabel(label1));
// Commit the first transaction.
ASSERT_NO_ERROR(acc1.Commit()); ASSERT_NO_ERROR(acc1.Commit());
} }
@ -589,7 +634,7 @@ TEST_F(ConstraintsTest, UniqueConstraintsLabelAlteration) {
ASSERT_TRUE(res.HasError()); ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(), EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1, (ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
prop1})); std::set{prop1}}));
} }
{ {
@ -613,12 +658,330 @@ TEST_F(ConstraintsTest, UniqueConstraintsLabelAlteration) {
ASSERT_NO_ERROR(vertex1->AddLabel(label1)); ASSERT_NO_ERROR(vertex1->AddLabel(label1));
ASSERT_NO_ERROR(vertex2->AddLabel(label1)); ASSERT_NO_ERROR(vertex2->AddLabel(label1));
// Reapply everything.
ASSERT_NO_ERROR(vertex1->RemoveLabel(label1));
ASSERT_NO_ERROR(vertex2->RemoveLabel(label1));
ASSERT_NO_ERROR(vertex1->AddLabel(label1));
ASSERT_NO_ERROR(vertex2->AddLabel(label1));
ASSERT_NO_ERROR(acc2.Commit()); ASSERT_NO_ERROR(acc2.Commit());
auto res = acc1.Commit(); auto res = acc1.Commit();
ASSERT_TRUE(res.HasError()); ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(), EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1, (ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
prop1})); std::set{prop1}}));
}
}
// NOLINTNEXTLINE(hicpp-special-member-functions)
TEST_F(ConstraintsTest, UniqueConstraintsPropertySetSize) {
{
// This should fail since unique constraint cannot be created for an empty
// property set.
auto res = storage.CreateUniqueConstraint(label1, {});
ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(),
UniqueConstraints::CreationStatus::INVALID_PROPERTIES_SIZE);
}
// Create a set of 33 properties.
std::set<PropertyId> properties;
for (int i = 1; i <= 33; ++i) {
properties.insert(storage.NameToProperty("prop" + std::to_string(i)));
}
{
// This should fail since list of properties exceeds the maximum number of
// properties, which is 32.
auto res = storage.CreateUniqueConstraint(label1, properties);
ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(),
UniqueConstraints::CreationStatus::INVALID_PROPERTIES_SIZE);
}
// Remove one property from the set.
properties.erase(properties.begin());
{
// Creating a constraint for 32 properties should succeed.
auto res = storage.CreateUniqueConstraint(label1, properties);
ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
}
EXPECT_THAT(storage.ListAllConstraints().unique,
UnorderedElementsAre(std::make_pair(label1, properties)));
}
// NOLINTNEXTLINE(hicpp-special-member-functions)
TEST_F(ConstraintsTest, UniqueConstraintsMultipleProperties) {
{
auto res = storage.CreateUniqueConstraint(label1, {prop1, prop2});
ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
}
{
// An attempt to create an existing unique constraint.
auto res = storage.CreateUniqueConstraint(label1, {prop2, prop1});
ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(),
UniqueConstraints::CreationStatus::ALREADY_EXISTS);
}
Gid gid1;
Gid gid2;
{
auto acc = storage.Access();
auto vertex1 = acc.CreateVertex();
auto vertex2 = acc.CreateVertex();
gid1 = vertex1.Gid();
gid2 = vertex2.Gid();
ASSERT_NO_ERROR(vertex1.AddLabel(label1));
ASSERT_NO_ERROR(vertex1.SetProperty(prop1, PropertyValue(1)));
ASSERT_NO_ERROR(vertex1.SetProperty(prop2, PropertyValue(2)));
ASSERT_NO_ERROR(vertex2.AddLabel(label1));
ASSERT_NO_ERROR(vertex2.SetProperty(prop1, PropertyValue(1)));
ASSERT_NO_ERROR(vertex2.SetProperty(prop2, PropertyValue(3)));
ASSERT_NO_ERROR(acc.Commit());
}
// Try to change property of the second vertex so it becomes the same as the
// first vertex. It should fail.
{
auto acc = storage.Access();
auto vertex2 = acc.FindVertex(gid2, View::OLD);
ASSERT_NO_ERROR(vertex2->SetProperty(prop2, PropertyValue(2)));
auto res = acc.Commit();
ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
std::set<PropertyId>{prop1, prop2}}));
}
// Then change the second property of both vertex to null. Property values of
// both vertices should now be equal. However, this operation should succeed
// since null value is treated as non-existing property.
{
auto acc = storage.Access();
auto vertex1 = acc.FindVertex(gid1, View::OLD);
auto vertex2 = acc.FindVertex(gid2, View::OLD);
ASSERT_NO_ERROR(vertex1->SetProperty(prop2, PropertyValue()));
ASSERT_NO_ERROR(vertex2->SetProperty(prop2, PropertyValue()));
ASSERT_NO_ERROR(acc.Commit());
}
}
TEST_F(ConstraintsTest, UniqueConstraintsInsertAbortInsert) {
{
auto res = storage.CreateUniqueConstraint(label1, {prop1, prop2});
ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
}
{
auto acc = storage.Access();
auto vertex = acc.CreateVertex();
ASSERT_NO_ERROR(vertex.AddLabel(label1));
ASSERT_NO_ERROR(vertex.SetProperty(prop1, PropertyValue(1)));
ASSERT_NO_ERROR(vertex.SetProperty(prop2, PropertyValue(2)));
acc.Abort();
}
{
auto acc = storage.Access();
auto vertex = acc.CreateVertex();
ASSERT_NO_ERROR(vertex.AddLabel(label1));
ASSERT_NO_ERROR(vertex.SetProperty(prop2, PropertyValue(2)));
ASSERT_NO_ERROR(vertex.SetProperty(prop1, PropertyValue(1)));
ASSERT_NO_ERROR(acc.Commit());
}
}
TEST_F(ConstraintsTest, UniqueConstraintsInsertRemoveInsert) {
{
auto res = storage.CreateUniqueConstraint(label1, {prop1, prop2});
ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
}
Gid gid;
{
auto acc = storage.Access();
auto vertex = acc.CreateVertex();
gid = vertex.Gid();
ASSERT_NO_ERROR(vertex.AddLabel(label1));
ASSERT_NO_ERROR(vertex.SetProperty(prop1, PropertyValue(1)));
ASSERT_NO_ERROR(vertex.SetProperty(prop2, PropertyValue(2)));
ASSERT_NO_ERROR(acc.Commit());
}
{
auto acc = storage.Access();
auto vertex = acc.FindVertex(gid, View::OLD);
ASSERT_NO_ERROR(acc.DeleteVertex(&*vertex));
ASSERT_NO_ERROR(acc.Commit());
}
{
auto acc = storage.Access();
auto vertex = acc.CreateVertex();
ASSERT_NO_ERROR(vertex.AddLabel(label1));
ASSERT_NO_ERROR(vertex.SetProperty(prop1, PropertyValue(1)));
ASSERT_NO_ERROR(vertex.SetProperty(prop2, PropertyValue(2)));
ASSERT_NO_ERROR(acc.Commit());
}
}
TEST_F(ConstraintsTest, UniqueConstraintsInsertRemoveAbortInsert) {
{
auto res = storage.CreateUniqueConstraint(label1, {prop1, prop2});
ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
}
Gid gid;
{
auto acc = storage.Access();
auto vertex = acc.CreateVertex();
gid = vertex.Gid();
ASSERT_NO_ERROR(vertex.AddLabel(label1));
ASSERT_NO_ERROR(vertex.SetProperty(prop1, PropertyValue(2)));
ASSERT_NO_ERROR(vertex.SetProperty(prop2, PropertyValue(1)));
ASSERT_NO_ERROR(acc.Commit());
}
{
auto acc = storage.Access();
auto vertex = acc.FindVertex(gid, View::OLD);
ASSERT_NO_ERROR(acc.DeleteVertex(&*vertex));
acc.Abort();
}
{
auto acc = storage.Access();
auto vertex = acc.CreateVertex();
ASSERT_NO_ERROR(vertex.AddLabel(label1));
ASSERT_NO_ERROR(vertex.SetProperty(prop2, PropertyValue(1)));
ASSERT_NO_ERROR(vertex.SetProperty(prop1, PropertyValue(2)));
auto res = acc.Commit();
ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
std::set{prop1, prop2}}));
}
}
TEST_F(ConstraintsTest, UniqueConstraintsDeleteVertexSetProperty) {
{
auto res = storage.CreateUniqueConstraint(label1, {prop1});
ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
}
Gid gid1;
Gid gid2;
{
auto acc = storage.Access();
auto vertex1 = acc.CreateVertex();
auto vertex2 = acc.CreateVertex();
gid1 = vertex1.Gid();
gid2 = vertex2.Gid();
ASSERT_NO_ERROR(vertex1.AddLabel(label1));
ASSERT_NO_ERROR(vertex2.AddLabel(label1));
ASSERT_NO_ERROR(vertex1.SetProperty(prop1, PropertyValue(1)));
ASSERT_NO_ERROR(vertex2.SetProperty(prop1, PropertyValue(2)));
ASSERT_NO_ERROR(acc.Commit());
}
{
auto acc1 = storage.Access();
auto acc2 = storage.Access();
auto vertex1 = acc1.FindVertex(gid1, View::OLD);
auto vertex2 = acc2.FindVertex(gid2, View::OLD);
ASSERT_NO_ERROR(acc2.DeleteVertex(&*vertex2));
ASSERT_NO_ERROR(vertex1->SetProperty(prop1, PropertyValue(2)));
auto res = acc1.Commit();
ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
std::set{prop1}}));
ASSERT_NO_ERROR(acc2.Commit());
}
}
TEST_F(ConstraintsTest, UniqueConstraintsInsertDropInsert) {
{
auto res = storage.CreateUniqueConstraint(label1, {prop1, prop2});
ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
}
{
auto acc = storage.Access();
auto vertex = acc.CreateVertex();
ASSERT_NO_ERROR(vertex.AddLabel(label1));
ASSERT_NO_ERROR(vertex.SetProperty(prop1, PropertyValue(1)));
ASSERT_NO_ERROR(vertex.SetProperty(prop2, PropertyValue(2)));
ASSERT_NO_ERROR(acc.Commit());
}
ASSERT_TRUE(storage.DropUniqueConstraint(label1, {prop2, prop1}));
{
auto acc = storage.Access();
auto vertex = acc.CreateVertex();
ASSERT_NO_ERROR(vertex.AddLabel(label1));
ASSERT_NO_ERROR(vertex.SetProperty(prop2, PropertyValue(2)));
ASSERT_NO_ERROR(vertex.SetProperty(prop1, PropertyValue(1)));
ASSERT_NO_ERROR(acc.Commit());
}
}
TEST_F(ConstraintsTest, UniqueConstraintsComparePropertyValues) {
// Purpose of this test is to make sure that extracted property values
// are correctly compared.
{
auto res = storage.CreateUniqueConstraint(label1, {prop1, prop2});
ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
}
{
auto acc = storage.Access();
auto vertex = acc.CreateVertex();
ASSERT_NO_ERROR(vertex.AddLabel(label1));
ASSERT_NO_ERROR(vertex.SetProperty(prop1, PropertyValue(2)));
ASSERT_NO_ERROR(vertex.SetProperty(prop2, PropertyValue(1)));
ASSERT_NO_ERROR(acc.Commit());
}
{
auto acc = storage.Access();
auto vertex = acc.CreateVertex();
ASSERT_NO_ERROR(vertex.AddLabel(label1));
ASSERT_NO_ERROR(vertex.SetProperty(prop1, PropertyValue(1)));
ASSERT_NO_ERROR(vertex.SetProperty(prop2, PropertyValue(2)));
ASSERT_NO_ERROR(acc.Commit());
}
{
auto acc = storage.Access();
auto vertex = acc.CreateVertex();
ASSERT_NO_ERROR(vertex.AddLabel(label1));
ASSERT_NO_ERROR(vertex.SetProperty(prop2, PropertyValue(0)));
ASSERT_NO_ERROR(vertex.SetProperty(prop1, PropertyValue(3)));
ASSERT_NO_ERROR(acc.Commit());
} }
} }