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

View File

@ -1,32 +1,62 @@
#include "storage/v2/constraints.hpp"
#include <set>
#include <algorithm>
#include <cstring>
#include <map>
#include "storage/v2/mvcc.hpp"
namespace storage {
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
/// the last commited version of the given vertex contains the given label and
/// property value. This function should be called when commit lock is active.
bool LastCommitedVersionHasLabelProperty(const Vertex &vertex, LabelId label,
PropertyId key, const PropertyValue &value,
const Transaction &transaction,
uint64_t commit_timestamp) {
/// the last committed version of the given vertex contains the given label and
/// set of property values. This function should be called when commit lock is
/// active.
bool LastCommittedVersionHasLabelProperty(
const Vertex &vertex, LabelId label, const std::set<PropertyId> &properties,
const PropertyValueArray &value_array, const Transaction &transaction,
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.
// Any transaction that tries to write to that vertex will result in
// serialization error.
bool deleted = vertex.deleted;
bool has_label = utils::Contains(vertex.labels, label);
bool current_value_equal_to_value = value.IsNull();
auto it = vertex.properties.find(key);
if (it != vertex.properties.end()) {
current_value_equal_to_value = it->second == value;
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()) {
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;
delta = delta->next.load(std::memory_order_acquire)) {
delta = delta->next.load(std::memory_order_acquire)) {
auto ts = delta->timestamp->load(std::memory_order_acquire);
if (ts < commit_timestamp || ts == transaction.transaction_id) {
break;
@ -34,8 +64,10 @@ bool LastCommitedVersionHasLabelProperty(const Vertex &vertex, LabelId label,
switch (delta->action) {
case Delta::Action::SET_PROPERTY: {
if (delta->property.key == key) {
current_value_equal_to_value = (delta->property.value == value);
auto pos = FindPropertyPosition(property_array, delta->property.key);
if (pos) {
current_value_equal_to_value[*pos] =
delta->property.value == *value_array.values[*pos];
}
break;
}
@ -63,40 +95,68 @@ bool LastCommitedVersionHasLabelProperty(const Vertex &vertex, LabelId label,
break;
}
}
case Delta::Action::ADD_IN_EDGE:
case Delta::Action::ADD_OUT_EDGE:
case Delta::Action::REMOVE_IN_EDGE:
case Delta::Action::REMOVE_OUT_EDGE:
break;
}
case Delta::Action::ADD_IN_EDGE:
case Delta::Action::ADD_OUT_EDGE:
case Delta::Action::REMOVE_IN_EDGE:
case Delta::Action::REMOVE_OUT_EDGE:
break;
}
}
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
/// 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,
PropertyId key, const PropertyValue &value,
const std::set<PropertyId> &properties,
const std::vector<PropertyValue> &values,
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 deleted;
bool current_value_equal_to_value = value.IsNull();
Delta *delta;
{
std::lock_guard<utils::SpinLock> guard(vertex.lock);
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;
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) {
return true;
{
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;
}
}
while (delta != nullptr) {
@ -117,11 +177,14 @@ bool AnyVersionHasLabelProperty(const Vertex &vertex, LabelId label,
has_label = false;
}
break;
case Delta::Action::SET_PROPERTY:
if (delta->property.key == key) {
current_value_equal_to_value = delta->property.value == value;
case Delta::Action::SET_PROPERTY: {
auto pos = FindPropertyPosition(property_array, delta->property.key);
if (pos) {
current_value_equal_to_value[*pos] =
delta->property.value == values[*pos];
}
break;
}
case Delta::Action::RECREATE_OBJECT: {
CHECK(deleted) << "Invalid database state!";
deleted = false;
@ -138,7 +201,15 @@ bool AnyVersionHasLabelProperty(const Vertex &vertex, LabelId label,
case Delta::Action::REMOVE_OUT_EDGE:
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;
}
delta = delta->next.load(std::memory_order_acquire);
@ -146,19 +217,94 @@ bool AnyVersionHasLabelProperty(const Vertex &vertex, LabelId label,
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
bool operator==(const ConstraintViolation &lhs,
const ConstraintViolation &rhs) {
return lhs.type == rhs.type && lhs.label == rhs.label &&
lhs.property == rhs.property;
lhs.properties == rhs.properties;
}
bool UniqueConstraints::Entry::operator<(const Entry &rhs) {
if (value < rhs.value) {
if (values < rhs.values) {
return true;
}
if (rhs.value < value) {
if (rhs.values < values) {
return false;
}
return std::make_tuple(vertex, timestamp) <
@ -166,42 +312,59 @@ 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;
}
bool UniqueConstraints::Entry::operator<(const PropertyValue &rhs) {
return value < rhs;
bool UniqueConstraints::Entry::operator<(
const std::vector<PropertyValue> &rhs) {
return values < rhs;
}
bool UniqueConstraints::Entry::operator==(const PropertyValue &rhs) {
return value == rhs;
bool UniqueConstraints::Entry::operator==(
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,
const Transaction &tx) {
for (auto &[label_prop, storage] : constraints_) {
if (!utils::Contains(vertex->labels, label_prop.first)) {
for (auto &[label_props, storage] : constraints_) {
if (!utils::Contains(vertex->labels, label_props.first)) {
continue;
}
auto it = vertex->properties.find(label_prop.second);
if (it != vertex->properties.end() && !it->second.IsNull()) {
auto values = OptionalPropertyValueArrayToVector(
ExtractPropertyValues(*vertex, label_props.second));
if (values) {
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(
LabelId label, PropertyId property,
utils::BasicResult<ConstraintViolation, UniqueConstraints::CreationStatus>
UniqueConstraints::CreateConstraint(
LabelId label, const std::set<PropertyId> &properties,
utils::SkipList<Vertex>::Accessor vertices) {
if (properties.empty() ||
properties.size() > kUniqueConstraintsMaxProperties) {
return CreationStatus::INVALID_PROPERTIES_SIZE;
}
auto [constraint, emplaced] = constraints_.emplace(
std::piecewise_construct,
std::forward_as_tuple(label, property),
std::forward_as_tuple());
std::piecewise_construct, std::forward_as_tuple(label, properties),
std::forward_as_tuple());
if (!emplaced) {
// Constraint already exists.
return false;
return CreationStatus::ALREADY_EXISTS;
}
bool violation_found = false;
@ -213,21 +376,21 @@ utils::BasicResult<ConstraintViolation, bool> UniqueConstraints::CreateConstrain
if (vertex.deleted || !utils::Contains(vertex.labels, label)) {
continue;
}
auto property_it = vertex.properties.find(property);
if (property_it == vertex.properties.end()) {
auto values = OptionalPropertyValueArrayToVector(
ExtractPropertyValues(vertex, properties));
if (!values) {
continue;
}
const PropertyValue &value = property_it->second;
// Check whether there already is a vertex with the same value for the given
// label and property.
auto it = acc.find_equal_or_greater(value);
if (it != acc.end() && it->value == value) {
// Check whether there already is a vertex with the same values for the
// given label and property.
auto it = acc.find_equal_or_greater(*values);
if (it != acc.end() && it->values == *values) {
violation_found = true;
break;
}
acc.insert(Entry{value, &vertex, 0});
acc.insert(Entry{std::move(*values), &vertex, 0});
}
}
@ -236,64 +399,63 @@ utils::BasicResult<ConstraintViolation, bool> UniqueConstraints::CreateConstrain
// be removed.
constraints_.erase(constraint);
return ConstraintViolation{ConstraintViolation::Type::UNIQUE, label,
property};
properties};
}
return true;
return CreationStatus::SUCCESS;
}
std::optional<ConstraintViolation> UniqueConstraints::Validate(
const Vertex &vertex, const Transaction &tx,
uint64_t commit_timestamp) const {
if (vertex.deleted) {
if (vertex.deleted) {
return std::nullopt;
}
for (const auto &[label_prop, storage] : constraints_) {
const auto &label = label_prop.first;
const auto &property = label_prop.second;
auto property_it = vertex.properties.find(property);
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.
for (const auto &[label_props, storage] : constraints_) {
const auto &label = label_props.first;
const auto &properties = label_props.second;
if (!utils::Contains(vertex.labels, label)) {
continue;
}
const auto &value = property_it->second;
auto value_array = ExtractPropertyValues(vertex, properties);
if (!value_array) {
continue;
}
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) {
if (value < it->value) {
if (*value_array < it->values) {
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
// 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.
if (&vertex != it->vertex &&
LastCommitedVersionHasLabelProperty(*it->vertex, label, property,
value, tx, commit_timestamp)) {
if (&vertex != it->vertex && LastCommittedVersionHasLabelProperty(
*it->vertex, label, properties,
*value_array, tx, commit_timestamp)) {
return ConstraintViolation{ConstraintViolation::Type::UNIQUE, label,
property};
properties};
}
}
}
return std::nullopt;
}
std::vector<std::pair<LabelId, PropertyId>> UniqueConstraints::ListConstraints()
const {
std::vector<std::pair<LabelId, PropertyId>> ret;
std::vector<std::pair<LabelId, std::set<PropertyId>>>
UniqueConstraints::ListConstraints() const {
std::vector<std::pair<LabelId, std::set<PropertyId>>> ret;
ret.reserve(constraints_.size());
for (const auto &item : constraints_) {
ret.push_back(item.first);
for (const auto &[label_props, _] : constraints_) {
ret.push_back(label_props);
}
return ret;
}
void UniqueConstraints::RemoveObsoleteEntries(
uint64_t oldest_active_start_timestamp) {
for (auto &[label_prop, storage] : constraints_) {
for (auto &[label_props, storage] : constraints_) {
auto acc = storage.access();
for (auto it = acc.begin(); it != acc.end();) {
auto next_it = it;
@ -305,9 +467,9 @@ void UniqueConstraints::RemoveObsoleteEntries(
}
if ((next_it != acc.end() && it->vertex == next_it->vertex &&
it->value == next_it->value) ||
!AnyVersionHasLabelProperty(*it->vertex, label_prop.first,
label_prop.second, it->value,
it->values == next_it->values) ||
!AnyVersionHasLabelProperty(*it->vertex, label_props.first,
label_props.second, it->values,
oldest_active_start_timestamp)) {
acc.remove(*it);
}

View File

@ -1,6 +1,7 @@
#pragma once
#include <optional>
#include <set>
#include <vector>
#include "storage/v2/id_types.hpp"
@ -11,6 +12,25 @@
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 {
enum class Type {
EXISTENCE,
@ -19,51 +39,65 @@ struct ConstraintViolation {
Type type;
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,
const ConstraintViolation &rhs);
bool operator==(const ConstraintViolation &lhs, 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 {
private:
struct Entry {
PropertyValue value;
std::vector<PropertyValue> values;
const Vertex *vertex;
uint64_t timestamp;
bool operator<(const Entry &rhs);
bool operator==(const Entry &rhs);
bool operator<(const PropertyValue &rhs);
bool operator==(const PropertyValue &rhs);
bool operator<(const std::vector<PropertyValue> &rhs);
bool operator==(const std::vector<PropertyValue> &rhs);
bool operator<(const PropertyValueArray &rhs);
bool operator==(const PropertyValueArray &rhs);
};
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.
/// This method should be called before committing and validating vertices
/// against unique constraints.
/// @throw std::bad_alloc
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
/// label and property values. Returns false if constraint already existed and
/// true on success.
/// label and property values. Returns `CreationStatus::ALREADY_EXISTS` if
/// 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
utils::BasicResult<ConstraintViolation, bool> CreateConstraint(
LabelId label, PropertyId property,
utils::BasicResult<ConstraintViolation, CreationStatus> CreateConstraint(
LabelId label, const std::set<PropertyId> &properties,
utils::SkipList<Vertex>::Accessor vertices);
bool DropConstraint(LabelId label, PropertyId property) {
return constraints_.erase({label, property}) > 0;
bool DropConstraint(LabelId label, const std::set<PropertyId> &properties) {
return constraints_.erase({label, properties}) > 0;
}
bool ConstraintExists(LabelId label, PropertyId property) const {
return constraints_.find({label, property}) != constraints_.end();
bool ConstraintExists(LabelId label, const std::set<PropertyId> &properties) {
return constraints_.find({label, properties}) != constraints_.end();
}
/// Validates the given vertex against unique constraints before committing.
@ -73,14 +107,16 @@ class UniqueConstraints {
const Transaction &tx,
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 Clear() { constraints_.clear(); }
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 {
@ -89,16 +125,15 @@ struct Constraints {
};
/// Adds a unique constraint to `constraints`. Returns true if the constraint
/// was successfuly added, false if it already exists and an
/// `ExistenceConstraintViolation` if there is an existing vertex violating the
/// was successfully added, false if it already exists and a
/// `ConstraintViolation` if there is an existing vertex violating the
/// constraint.
///
/// @throw std::bad_alloc
/// @throw std::length_error
inline utils::BasicResult<ConstraintViolation, bool>
CreateExistenceConstraint(Constraints *constraints, LabelId label,
PropertyId property,
utils::SkipList<Vertex>::Accessor vertices) {
inline utils::BasicResult<ConstraintViolation, bool> CreateExistenceConstraint(
Constraints *constraints, LabelId label, PropertyId property,
utils::SkipList<Vertex>::Accessor vertices) {
if (utils::Contains(constraints->existence_constraints,
std::make_pair(label, property))) {
return false;
@ -107,7 +142,7 @@ CreateExistenceConstraint(Constraints *constraints, LabelId label,
if (!vertex.deleted && utils::Contains(vertex.labels, label) &&
vertex.properties.find(property) == vertex.properties.end()) {
return ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label,
property};
std::set<PropertyId>{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
/// nullopt if all checks pass, and `ExistenceConstraintViolation` describing
/// the violated constraint otherwise.
/// `std::nullopt` if all checks pass, and `ConstraintViolation` describing the
/// violated constraint otherwise.
[[nodiscard]] inline std::optional<ConstraintViolation>
ValidateExistenceConstraints(const Vertex &vertex,
const Constraints &constraints) {
@ -138,7 +173,7 @@ ValidateExistenceConstraints(const Vertex &vertex,
if (!vertex.deleted && utils::Contains(vertex.labels, label) &&
vertex.properties.find(property) == vertex.properties.end()) {
return ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label,
property};
std::set<PropertyId>{property}};
}
}
return std::nullopt;

View File

@ -17,7 +17,8 @@ auto AdvanceToVisibleVertex(utils::SkipList<Vertex>::Iterator it,
Transaction *tx, View view, Indices *indices,
Constraints *constraints, Config::Items config) {
while (it != end) {
*vertex = VertexAccessor::Create(&*it, tx, indices, constraints, config, view);
*vertex =
VertexAccessor::Create(&*it, tx, indices, constraints, config, view);
if (!*vertex) {
++it;
continue;
@ -638,8 +639,7 @@ EdgeTypeId Storage::Accessor::NameToEdgeType(const std::string &name) {
void Storage::Accessor::AdvanceCommand() { ++transaction_.command_id; }
utils::BasicResult<ConstraintViolation, void>
Storage::Accessor::Commit() {
utils::BasicResult<ConstraintViolation, void> Storage::Accessor::Commit() {
CHECK(is_transaction_active_) << "The transaction is already terminated!";
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
// one else can touch it until we commit.
unique_constraint_violation = storage_->constraints_.unique_constraints
.Validate(*prev.vertex, transaction_, commit_timestamp);
unique_constraint_violation =
storage_->constraints_.unique_constraints.Validate(
*prev.vertex, transaction_, commit_timestamp);
if (unique_constraint_violation) {
break;
}
@ -712,8 +713,8 @@ Storage::Accessor::Commit() {
// that committed transactions are sorted by the commit timestamp in the
// 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
// written before actually committing the transaction (before setting the
// commit timestamp) so that no other transaction can see the
// written before actually committing the transaction (before setting
// the commit timestamp) so that no other transaction can see the
// modifications before they are written to disk.
storage_->durability_.AppendToWal(transaction_, commit_timestamp);
@ -727,9 +728,9 @@ Storage::Accessor::Commit() {
CHECK(transaction_.commit_timestamp != nullptr)
<< "Invalid database state!";
transaction_.commit_timestamp->store(commit_timestamp,
std::memory_order_release);
// Release engine lock because we don't have to hold it anymore and
// emplace back could take a long time.
std::memory_order_release);
// Release engine lock because we don't have to hold it anymore
// and emplace back could take a long time.
engine_guard.unlock();
committed_transactions.emplace_back(std::move(transaction_));
});
@ -1056,20 +1057,20 @@ bool Storage::DropExistenceConstraint(LabelId label, PropertyId property) {
return true;
}
utils::BasicResult<ConstraintViolation, bool>
Storage::CreateUniqueConstraint(LabelId label, PropertyId property) {
utils::BasicResult<ConstraintViolation, UniqueConstraints::CreationStatus>
Storage::CreateUniqueConstraint(LabelId label,
const std::set<PropertyId> &properties) {
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.
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_);
// 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 {

View File

@ -146,7 +146,7 @@ struct IndicesInfo {
/// storage.
struct ConstraintsInfo {
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.
@ -344,24 +344,29 @@ class Storage final {
///
/// @throw std::bad_alloc
/// @throw std::length_error
utils::BasicResult<ConstraintViolation, bool>
CreateExistenceConstraint(LabelId label, PropertyId property);
utils::BasicResult<ConstraintViolation, bool> CreateExistenceConstraint(
LabelId label, PropertyId property);
/// Removes an existence constraint. Returns true if the constraint was
/// removed, and false if it doesn't exist.
bool DropExistenceConstraint(LabelId label, PropertyId property);
/// Creates a unique constraint. Returns true if the constraint was
/// successfully added, false if it already exists and a `ConstraintViolation`
/// if there are at least two vertices violating the constraint.
/// Creates a unique constraint. In the case of two vertices violating the
/// constraint, it returns `ConstraintViolation`. Otherwise returns a
/// `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
utils::BasicResult<ConstraintViolation, bool>
CreateUniqueConstraint(LabelId label, PropertyId property);
utils::BasicResult<ConstraintViolation, UniqueConstraints::CreationStatus>
CreateUniqueConstraint(LabelId label, const std::set<PropertyId> &properties);
/// Removes a unique constraint. Returns true if the constraint was removed,
/// 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;

View File

@ -11,12 +11,6 @@ using testing::UnorderedElementsAre;
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#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 {
protected:
ConstraintsTest()
@ -82,7 +76,7 @@ TEST_F(ConstraintsTest, ExistenceConstraintsCreateFailure1) {
ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label1,
prop1}));
std::set<PropertyId>{prop1}}));
}
{
auto acc = storage.Access();
@ -110,7 +104,7 @@ TEST_F(ConstraintsTest, ExistenceConstraintsCreateFailure2) {
ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label1,
prop1}));
std::set<PropertyId>{prop1}}));
}
{
auto acc = storage.Access();
@ -141,7 +135,7 @@ TEST_F(ConstraintsTest, ExistenceConstraintsViolationOnCommit) {
ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label1,
prop1}));
std::set<PropertyId>{prop1}}));
}
{
@ -162,7 +156,7 @@ TEST_F(ConstraintsTest, ExistenceConstraintsViolationOnCommit) {
ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label1,
prop1}));
std::set<PropertyId>{prop1}}));
}
{
@ -191,37 +185,48 @@ TEST_F(ConstraintsTest, ExistenceConstraintsViolationOnCommit) {
TEST_F(ConstraintsTest, UniqueConstraintsCreateAndDrop) {
EXPECT_EQ(storage.ListAllConstraints().unique.size(), 0);
{
auto res = storage.CreateUniqueConstraint(label1, prop1);
EXPECT_TRUE(res.HasValue() && res.GetValue());
auto res = storage.CreateUniqueConstraint(label1, {prop1});
EXPECT_TRUE(res.HasValue());
EXPECT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
}
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);
EXPECT_TRUE(res.HasValue() && !res.GetValue());
auto res = storage.CreateUniqueConstraint(label1, {prop1});
EXPECT_TRUE(res.HasValue());
EXPECT_EQ(res.GetValue(),
UniqueConstraints::CreationStatus::ALREADY_EXISTS);
}
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);
EXPECT_TRUE(res.HasValue() && res.GetValue());
auto res = storage.CreateUniqueConstraint(label2, {prop1});
EXPECT_TRUE(res.HasValue() &&
res.GetValue() == UniqueConstraints::CreationStatus::SUCCESS);
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
}
EXPECT_THAT(storage.ListAllConstraints().unique,
UnorderedElementsAre(std::make_pair(label1, prop1),
std::make_pair(label2, prop1)));
EXPECT_TRUE(storage.DropUniqueConstraint(label1, prop1));
EXPECT_FALSE(storage.DropUniqueConstraint(label1, prop1));
UnorderedElementsAre(
std::make_pair(label1, std::set<PropertyId>{prop1}),
std::make_pair(label2, std::set<PropertyId>{prop1})));
EXPECT_TRUE(storage.DropUniqueConstraint(label1, {prop1}));
EXPECT_FALSE(storage.DropUniqueConstraint(label1, {prop1}));
EXPECT_THAT(storage.ListAllConstraints().unique,
UnorderedElementsAre(std::make_pair(label2, prop1)));
EXPECT_TRUE(storage.DropUniqueConstraint(label2, prop1));
EXPECT_FALSE(storage.DropUniqueConstraint(label2, prop2));
UnorderedElementsAre(
std::make_pair(label2, std::set<PropertyId>{prop1})));
EXPECT_TRUE(storage.DropUniqueConstraint(label2, {prop1}));
EXPECT_FALSE(storage.DropUniqueConstraint(label2, {prop2}));
EXPECT_EQ(storage.ListAllConstraints().unique.size(), 0);
{
auto res = storage.CreateUniqueConstraint(label2, prop1);
EXPECT_TRUE(res.HasValue() && res.GetValue());
auto res = storage.CreateUniqueConstraint(label2, {prop1});
EXPECT_TRUE(res.HasValue());
EXPECT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
}
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)
@ -237,11 +242,11 @@ TEST_F(ConstraintsTest, UniqueConstraintsCreateFailure1) {
}
{
auto res = storage.CreateUniqueConstraint(label1, prop1);
auto res = storage.CreateUniqueConstraint(label1, {prop1});
ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
prop1}));
std::set<PropertyId>{prop1}}));
}
{
@ -253,8 +258,9 @@ TEST_F(ConstraintsTest, UniqueConstraintsCreateFailure1) {
}
{
auto res = storage.CreateUniqueConstraint(label1, prop1);
EXPECT_TRUE(!res.HasError() && res.GetValue());
auto res = storage.CreateUniqueConstraint(label1, {prop1});
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());
EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
prop1}));
std::set<PropertyId>{prop1}}));
}
{
@ -289,18 +295,14 @@ TEST_F(ConstraintsTest, UniqueConstraintsCreateFailure2) {
}
{
auto res = storage.CreateUniqueConstraint(label1, prop1);
EXPECT_TRUE(!res.HasError() && res.GetValue());
auto res = storage.CreateUniqueConstraint(label1, {prop1});
ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
}
}
// NOLINTNEXTLINE(hicpp-special-member-functions)
TEST_F(ConstraintsTest, UniqueConstraintsNoViolation1) {
{
auto res = storage.CreateUniqueConstraint(label1, prop1);
ASSERT_TRUE(res.HasValue() && res.GetValue());
}
Gid gid1;
Gid gid2;
{
@ -309,10 +311,27 @@ TEST_F(ConstraintsTest, UniqueConstraintsNoViolation1) {
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(vertex2.AddLabel(label1));
ASSERT_NO_ERROR(vertex2.SetProperty(prop1, PropertyValue(2)));
ASSERT_NO_ERROR(acc.Commit());
}
{
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());
}
@ -329,8 +348,9 @@ TEST_F(ConstraintsTest, UniqueConstraintsNoViolation1) {
// NOLINTNEXTLINE(hicpp-special-member-functions)
TEST_F(ConstraintsTest, UniqueConstraintsNoViolation2) {
{
auto res = storage.CreateUniqueConstraint(label1, prop1);
ASSERT_TRUE(res.HasValue() && res.GetValue());
auto res = storage.CreateUniqueConstraint(label1, {prop1});
ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
}
{
@ -358,8 +378,9 @@ TEST_F(ConstraintsTest, UniqueConstraintsNoViolation2) {
// NOLINTNEXTLINE(hicpp-special-member-functions)
TEST_F(ConstraintsTest, UniqueConstraintsNoViolation3) {
{
auto res = storage.CreateUniqueConstraint(label1, prop1);
ASSERT_TRUE(res.HasValue() && res.GetValue());
auto res = storage.CreateUniqueConstraint(label1, {prop1});
ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
}
{
@ -393,8 +414,9 @@ TEST_F(ConstraintsTest, UniqueConstraintsNoViolation3) {
// NOLINTNEXTLINE(hicpp-special-member-functions)
TEST_F(ConstraintsTest, UniqueConstraintsNoViolation4) {
{
auto res = storage.CreateUniqueConstraint(label1, prop1);
ASSERT_TRUE(res.HasValue() && res.GetValue());
auto res = storage.CreateUniqueConstraint(label1, {prop1});
ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
}
{
@ -428,8 +450,9 @@ TEST_F(ConstraintsTest, UniqueConstraintsNoViolation4) {
// NOLINTNEXTLINE(hicpp-special-member-functions)
TEST_F(ConstraintsTest, UniqueConstraintsViolationOnCommit1) {
{
auto res = storage.CreateUniqueConstraint(label1, prop1);
ASSERT_TRUE(res.HasValue() && res.GetValue());
auto res = storage.CreateUniqueConstraint(label1, {prop1});
ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
}
{
@ -444,15 +467,16 @@ TEST_F(ConstraintsTest, UniqueConstraintsViolationOnCommit1) {
ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
prop1}));
std::set<PropertyId>{prop1}}));
}
}
// NOLINTNEXTLINE(hicpp-special-member-functions)
TEST_F(ConstraintsTest, UniqueConstraintsViolationOnCommit2) {
{
auto res = storage.CreateUniqueConstraint(label1, prop1);
ASSERT_TRUE(res.HasValue() && res.GetValue());
auto res = storage.CreateUniqueConstraint(label1, {prop1});
ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
}
{
@ -486,15 +510,16 @@ TEST_F(ConstraintsTest, UniqueConstraintsViolationOnCommit2) {
ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
prop1}));
std::set<PropertyId>{prop1}}));
}
}
// NOLINTNEXTLINE(hicpp-special-member-functions)
TEST_F(ConstraintsTest, UniqueConstraintsViolationOnCommit3) {
{
auto res = storage.CreateUniqueConstraint(label1, prop1);
ASSERT_TRUE(res.HasValue() & res.GetValue());
auto res = storage.CreateUniqueConstraint(label1, {prop1});
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 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(vertex4->SetProperty(prop1, PropertyValue(1)));
@ -527,20 +556,21 @@ TEST_F(ConstraintsTest, UniqueConstraintsViolationOnCommit3) {
ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
prop1}));
std::set<PropertyId>{prop1}}));
res = acc3.Commit();
ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(),
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
prop1}));
std::set<PropertyId>{prop1}}));
}
}
// NOLINTNEXTLINE(hicpp-special-member-functions)
TEST_F(ConstraintsTest, UniqueConstraintsLabelAlteration) {
{
auto res = storage.CreateUniqueConstraint(label1, prop1);
ASSERT_TRUE(res.HasValue() && res.GetValue());
auto res = storage.CreateUniqueConstraint(label1, {prop1});
ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(), UniqueConstraints::CreationStatus::SUCCESS);
}
Gid gid1;
@ -574,7 +604,22 @@ TEST_F(ConstraintsTest, UniqueConstraintsLabelAlteration) {
ASSERT_NO_ERROR(vertex1->AddLabel(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());
// 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());
}
@ -589,7 +634,7 @@ TEST_F(ConstraintsTest, UniqueConstraintsLabelAlteration) {
ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(),
(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(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());
auto res = acc1.Commit();
ASSERT_TRUE(res.HasError());
EXPECT_EQ(res.GetError(),
(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());
}
}