Add basic support for unique constraints in storage
Summary: This diff contains a basic implementation of unique constraints consistent with the MVCC storage. Stale records in the unique constraints are collected by the garbage collector. Tests for checking correctness of unique constraints and violations are included. Note: currently we only support a pair of label and a single property. Support for multiple properties will be added later. Reviewers: mferencevic, teon.banek Reviewed By: mferencevic, teon.banek Subscribers: buda, ipaljak, pullbot Differential Revision: https://phabricator.memgraph.io/D2608
This commit is contained in:
parent
ab53cbb0c3
commit
e2e7823ec4
@ -302,7 +302,7 @@ class DbAccessor final {
|
||||
|
||||
void AdvanceCommand() { accessor_->AdvanceCommand(); }
|
||||
|
||||
utils::BasicResult<storage::ExistenceConstraintViolation, void> Commit() {
|
||||
utils::BasicResult<storage::ConstraintViolation, void> Commit() {
|
||||
return accessor_->Commit();
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
set(storage_v2_src_files
|
||||
constraints.cpp
|
||||
durability.cpp
|
||||
edge_accessor.cpp
|
||||
indices.cpp
|
||||
|
319
src/storage/v2/constraints.cpp
Normal file
319
src/storage/v2/constraints.cpp
Normal file
@ -0,0 +1,319 @@
|
||||
#include "storage/v2/constraints.hpp"
|
||||
|
||||
#include <set>
|
||||
|
||||
#include "storage/v2/mvcc.hpp"
|
||||
|
||||
namespace storage {
|
||||
namespace {
|
||||
|
||||
/// 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) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
for (Delta *delta = vertex.delta; delta != nullptr;
|
||||
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;
|
||||
}
|
||||
|
||||
switch (delta->action) {
|
||||
case Delta::Action::SET_PROPERTY: {
|
||||
if (delta->property.key == key) {
|
||||
current_value_equal_to_value = (delta->property.value == value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Delta::Action::DELETE_OBJECT: {
|
||||
CHECK(!deleted) << "Invalid database state!";
|
||||
deleted = true;
|
||||
break;
|
||||
}
|
||||
case Delta::Action::RECREATE_OBJECT: {
|
||||
CHECK(deleted) << "Invalid database state!";
|
||||
deleted = false;
|
||||
break;
|
||||
}
|
||||
case Delta::Action::ADD_LABEL: {
|
||||
if (delta->label == label) {
|
||||
CHECK(!has_label) << "Invalid database state!";
|
||||
has_label = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
case Delta::Action::REMOVE_LABEL: {
|
||||
if (delta->label == label) {
|
||||
CHECK(has_label) << "Invalid database state!";
|
||||
has_label = false;
|
||||
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;
|
||||
}
|
||||
|
||||
/// 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.
|
||||
bool AnyVersionHasLabelProperty(const Vertex &vertex, LabelId label,
|
||||
PropertyId key, const PropertyValue &value,
|
||||
uint64_t timestamp) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (!deleted && has_label && current_value_equal_to_value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
while (delta != nullptr) {
|
||||
auto ts = delta->timestamp->load(std::memory_order_acquire);
|
||||
if (ts < timestamp) {
|
||||
break;
|
||||
}
|
||||
switch (delta->action) {
|
||||
case Delta::Action::ADD_LABEL:
|
||||
if (delta->label == label) {
|
||||
CHECK(!has_label) << "Invalid database state!";
|
||||
has_label = true;
|
||||
}
|
||||
break;
|
||||
case Delta::Action::REMOVE_LABEL:
|
||||
if (delta->label == label) {
|
||||
CHECK(has_label) << "Invalid database state!";
|
||||
has_label = false;
|
||||
}
|
||||
break;
|
||||
case Delta::Action::SET_PROPERTY:
|
||||
if (delta->property.key == key) {
|
||||
current_value_equal_to_value = delta->property.value == value;
|
||||
}
|
||||
break;
|
||||
case Delta::Action::RECREATE_OBJECT: {
|
||||
CHECK(deleted) << "Invalid database state!";
|
||||
deleted = false;
|
||||
break;
|
||||
}
|
||||
case Delta::Action::DELETE_OBJECT: {
|
||||
CHECK(!deleted) << "Invalid database state!";
|
||||
deleted = true;
|
||||
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;
|
||||
}
|
||||
if (!deleted && has_label && current_value_equal_to_value) {
|
||||
return true;
|
||||
}
|
||||
delta = delta->next.load(std::memory_order_acquire);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool operator==(const ConstraintViolation &lhs,
|
||||
const ConstraintViolation &rhs) {
|
||||
return lhs.type == rhs.type && lhs.label == rhs.label &&
|
||||
lhs.property == rhs.property;
|
||||
}
|
||||
|
||||
bool UniqueConstraints::Entry::operator<(const Entry &rhs) {
|
||||
if (value < rhs.value) {
|
||||
return true;
|
||||
}
|
||||
if (rhs.value < value) {
|
||||
return false;
|
||||
}
|
||||
return std::make_tuple(vertex, timestamp) <
|
||||
std::make_tuple(rhs.vertex, rhs.timestamp);
|
||||
}
|
||||
|
||||
bool UniqueConstraints::Entry::operator==(const Entry &rhs) {
|
||||
return value == rhs.value && vertex == rhs.vertex &&
|
||||
timestamp == rhs.timestamp;
|
||||
}
|
||||
|
||||
bool UniqueConstraints::Entry::operator<(const PropertyValue &rhs) {
|
||||
return value < rhs;
|
||||
}
|
||||
|
||||
bool UniqueConstraints::Entry::operator==(const PropertyValue &rhs) {
|
||||
return value == rhs;
|
||||
}
|
||||
|
||||
void UniqueConstraints::UpdateBeforeCommit(const Vertex *vertex,
|
||||
const Transaction &tx) {
|
||||
for (auto &[label_prop, storage] : constraints_) {
|
||||
if (!utils::Contains(vertex->labels, label_prop.first)) {
|
||||
continue;
|
||||
}
|
||||
auto it = vertex->properties.find(label_prop.second);
|
||||
if (it != vertex->properties.end() && !it->second.IsNull()) {
|
||||
auto acc = storage.access();
|
||||
acc.insert(Entry{it->second, vertex, tx.start_timestamp});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
utils::BasicResult<ConstraintViolation, bool> UniqueConstraints::CreateConstraint(
|
||||
LabelId label, PropertyId property,
|
||||
utils::SkipList<Vertex>::Accessor vertices) {
|
||||
auto [constraint, emplaced] = constraints_.emplace(
|
||||
std::piecewise_construct,
|
||||
std::forward_as_tuple(label, property),
|
||||
std::forward_as_tuple());
|
||||
if (!emplaced) {
|
||||
// Constraint already exists.
|
||||
return false;
|
||||
}
|
||||
|
||||
bool violation_found = false;
|
||||
|
||||
{
|
||||
auto acc = constraint->second.access();
|
||||
|
||||
for (const Vertex &vertex : vertices) {
|
||||
if (vertex.deleted || !utils::Contains(vertex.labels, label)) {
|
||||
continue;
|
||||
}
|
||||
auto property_it = vertex.properties.find(property);
|
||||
if (property_it == vertex.properties.end()) {
|
||||
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) {
|
||||
violation_found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
acc.insert(Entry{value, &vertex, 0});
|
||||
}
|
||||
}
|
||||
|
||||
if (violation_found) {
|
||||
// In the case of the violation, storage for the current constraint has to
|
||||
// be removed.
|
||||
constraints_.erase(constraint);
|
||||
return ConstraintViolation{ConstraintViolation::Type::UNIQUE, label,
|
||||
property};
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<ConstraintViolation> UniqueConstraints::Validate(
|
||||
const Vertex &vertex, const Transaction &tx,
|
||||
uint64_t commit_timestamp) const {
|
||||
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.
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto &value = property_it->second;
|
||||
auto acc = storage.access();
|
||||
auto it = acc.find_equal_or_greater(value);
|
||||
for (; it != acc.end(); ++it) {
|
||||
if (value < it->value) {
|
||||
break;
|
||||
}
|
||||
|
||||
// The `vertex` that is going to be commited 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
|
||||
// the vertex from the list.
|
||||
if (&vertex != it->vertex &&
|
||||
LastCommitedVersionHasLabelProperty(*it->vertex, label, property,
|
||||
value, tx, commit_timestamp)) {
|
||||
return ConstraintViolation{ConstraintViolation::Type::UNIQUE, label,
|
||||
property};
|
||||
}
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<std::pair<LabelId, PropertyId>> UniqueConstraints::ListConstraints()
|
||||
const {
|
||||
std::vector<std::pair<LabelId, PropertyId>> ret;
|
||||
ret.reserve(constraints_.size());
|
||||
for (const auto &item : constraints_) {
|
||||
ret.push_back(item.first);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void UniqueConstraints::RemoveObsoleteEntries(
|
||||
uint64_t oldest_active_start_timestamp) {
|
||||
for (auto &[label_prop, storage] : constraints_) {
|
||||
auto acc = storage.access();
|
||||
for (auto it = acc.begin(); it != acc.end();) {
|
||||
auto next_it = it;
|
||||
++next_it;
|
||||
|
||||
if (it->timestamp >= oldest_active_start_timestamp) {
|
||||
it = next_it;
|
||||
continue;
|
||||
}
|
||||
|
||||
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,
|
||||
oldest_active_start_timestamp)) {
|
||||
acc.remove(*it);
|
||||
}
|
||||
it = next_it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace storage
|
@ -4,21 +4,90 @@
|
||||
#include <vector>
|
||||
|
||||
#include "storage/v2/id_types.hpp"
|
||||
#include "storage/v2/transaction.hpp"
|
||||
#include "storage/v2/vertex.hpp"
|
||||
#include "utils/result.hpp"
|
||||
#include "utils/skip_list.hpp"
|
||||
|
||||
namespace storage {
|
||||
|
||||
struct Constraints {
|
||||
std::vector<std::pair<LabelId, PropertyId>> existence_constraints;
|
||||
};
|
||||
struct ConstraintViolation {
|
||||
enum class Type {
|
||||
EXISTENCE,
|
||||
UNIQUE,
|
||||
};
|
||||
|
||||
struct ExistenceConstraintViolation {
|
||||
Type type;
|
||||
LabelId label;
|
||||
PropertyId property;
|
||||
};
|
||||
|
||||
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;
|
||||
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);
|
||||
};
|
||||
|
||||
public:
|
||||
/// 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`.
|
||||
/// 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.
|
||||
/// @throw std::bad_alloc
|
||||
utils::BasicResult<ConstraintViolation, bool> CreateConstraint(
|
||||
LabelId label, PropertyId property,
|
||||
utils::SkipList<Vertex>::Accessor vertices);
|
||||
|
||||
bool DropConstraint(LabelId label, PropertyId property) {
|
||||
return constraints_.erase({label, property}) > 0;
|
||||
}
|
||||
|
||||
bool ConstraintExists(LabelId label, PropertyId property) const {
|
||||
return constraints_.find({label, property}) != constraints_.end();
|
||||
}
|
||||
|
||||
/// Validates the given vertex against unique constraints before committing.
|
||||
/// This method should be called while commit lock is active with
|
||||
/// `commit_timestamp` being a potential commit timestamp of the transaction.
|
||||
std::optional<ConstraintViolation> Validate(const Vertex &vertex,
|
||||
const Transaction &tx,
|
||||
uint64_t commit_timestamp) const;
|
||||
|
||||
std::vector<std::pair<LabelId, PropertyId>> ListConstraints() const;
|
||||
|
||||
void RemoveObsoleteEntries(uint64_t oldest_active_start_timestamp);
|
||||
|
||||
void Clear() { constraints_.clear(); }
|
||||
|
||||
private:
|
||||
std::map<std::pair<LabelId, PropertyId>, utils::SkipList<Entry>> constraints_;
|
||||
};
|
||||
|
||||
struct Constraints {
|
||||
std::vector<std::pair<LabelId, PropertyId>> existence_constraints;
|
||||
UniqueConstraints unique_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
|
||||
@ -26,7 +95,7 @@ struct ExistenceConstraintViolation {
|
||||
///
|
||||
/// @throw std::bad_alloc
|
||||
/// @throw std::length_error
|
||||
inline utils::BasicResult<ExistenceConstraintViolation, bool>
|
||||
inline utils::BasicResult<ConstraintViolation, bool>
|
||||
CreateExistenceConstraint(Constraints *constraints, LabelId label,
|
||||
PropertyId property,
|
||||
utils::SkipList<Vertex>::Accessor vertices) {
|
||||
@ -37,7 +106,8 @@ CreateExistenceConstraint(Constraints *constraints, LabelId label,
|
||||
for (const auto &vertex : vertices) {
|
||||
if (!vertex.deleted && utils::Contains(vertex.labels, label) &&
|
||||
vertex.properties.find(property) == vertex.properties.end()) {
|
||||
return ExistenceConstraintViolation{label, property};
|
||||
return ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label,
|
||||
property};
|
||||
}
|
||||
}
|
||||
constraints->existence_constraints.emplace_back(label, property);
|
||||
@ -61,13 +131,14 @@ 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.
|
||||
[[nodiscard]] inline std::optional<ExistenceConstraintViolation>
|
||||
[[nodiscard]] inline std::optional<ConstraintViolation>
|
||||
ValidateExistenceConstraints(const Vertex &vertex,
|
||||
const Constraints &constraints) {
|
||||
for (const auto &[label, property] : constraints.existence_constraints) {
|
||||
if (!vertex.deleted && utils::Contains(vertex.labels, label) &&
|
||||
vertex.properties.find(property) == vertex.properties.end()) {
|
||||
return ExistenceConstraintViolation{label, property};
|
||||
return ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label,
|
||||
property};
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
|
@ -1621,10 +1621,10 @@ void Durability::CreateSnapshot(Transaction *transaction) {
|
||||
// type and invalid from/to pointers because we don't know them here,
|
||||
// but that isn't an issue because we won't use that part of the API
|
||||
// here.
|
||||
auto ea = EdgeAccessor{edge_ref, EdgeTypeId::FromUint(0UL),
|
||||
nullptr, nullptr,
|
||||
transaction, indices_,
|
||||
items_};
|
||||
auto ea = EdgeAccessor{edge_ref, EdgeTypeId::FromUint(0UL),
|
||||
nullptr, nullptr,
|
||||
transaction, indices_,
|
||||
constraints_, items_};
|
||||
|
||||
// Get edge data.
|
||||
auto maybe_props = ea.Properties(View::OLD);
|
||||
@ -1652,8 +1652,8 @@ void Durability::CreateSnapshot(Transaction *transaction) {
|
||||
auto acc = vertices_->access();
|
||||
for (auto &vertex : acc) {
|
||||
// The visibility check is implemented for vertices so we use it here.
|
||||
auto va = VertexAccessor::Create(&vertex, transaction, indices_, items_,
|
||||
View::OLD);
|
||||
auto va = VertexAccessor::Create(&vertex, transaction, indices_,
|
||||
constraints_, items_, View::OLD);
|
||||
if (!va) continue;
|
||||
|
||||
// Get vertex data.
|
||||
|
@ -8,11 +8,11 @@
|
||||
namespace storage {
|
||||
|
||||
VertexAccessor EdgeAccessor::FromVertex() const {
|
||||
return VertexAccessor{from_vertex_, transaction_, indices_, config_};
|
||||
return VertexAccessor{from_vertex_, transaction_, indices_, constraints_, config_};
|
||||
}
|
||||
|
||||
VertexAccessor EdgeAccessor::ToVertex() const {
|
||||
return VertexAccessor{to_vertex_, transaction_, indices_, config_};
|
||||
return VertexAccessor{to_vertex_, transaction_, indices_, constraints_, config_};
|
||||
}
|
||||
|
||||
Result<bool> EdgeAccessor::SetProperty(PropertyId property,
|
||||
|
@ -15,6 +15,7 @@ namespace storage {
|
||||
struct Vertex;
|
||||
class VertexAccessor;
|
||||
struct Indices;
|
||||
struct Constraints;
|
||||
|
||||
class EdgeAccessor final {
|
||||
private:
|
||||
@ -23,13 +24,14 @@ class EdgeAccessor final {
|
||||
public:
|
||||
EdgeAccessor(EdgeRef edge, EdgeTypeId edge_type, Vertex *from_vertex,
|
||||
Vertex *to_vertex, Transaction *transaction, Indices *indices,
|
||||
Config::Items config)
|
||||
Constraints *constraints, Config::Items config)
|
||||
: edge_(edge),
|
||||
edge_type_(edge_type),
|
||||
from_vertex_(from_vertex),
|
||||
to_vertex_(to_vertex),
|
||||
transaction_(transaction),
|
||||
indices_(indices),
|
||||
constraints_(constraints),
|
||||
config_(config) {}
|
||||
|
||||
VertexAccessor FromVertex() const;
|
||||
@ -76,6 +78,7 @@ class EdgeAccessor final {
|
||||
Vertex *to_vertex_;
|
||||
Transaction *transaction_;
|
||||
Indices *indices_;
|
||||
Constraints *constraints_;
|
||||
Config::Items config_;
|
||||
};
|
||||
|
||||
|
@ -327,7 +327,8 @@ LabelIndex::Iterable::Iterator::Iterator(
|
||||
Iterable *self, utils::SkipList<Entry>::Iterator index_iterator)
|
||||
: self_(self),
|
||||
index_iterator_(index_iterator),
|
||||
current_vertex_accessor_(nullptr, nullptr, nullptr, self_->config_),
|
||||
current_vertex_accessor_(nullptr, nullptr, nullptr, nullptr,
|
||||
self_->config_),
|
||||
current_vertex_(nullptr) {
|
||||
AdvanceUntilValid();
|
||||
}
|
||||
@ -348,7 +349,7 @@ void LabelIndex::Iterable::Iterator::AdvanceUntilValid() {
|
||||
current_vertex_ = index_iterator_->vertex;
|
||||
current_vertex_accessor_ =
|
||||
VertexAccessor{current_vertex_, self_->transaction_, self_->indices_,
|
||||
self_->config_};
|
||||
self_->constraints_, self_->config_};
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -357,12 +358,13 @@ void LabelIndex::Iterable::Iterator::AdvanceUntilValid() {
|
||||
LabelIndex::Iterable::Iterable(utils::SkipList<Entry>::Accessor index_accessor,
|
||||
LabelId label, View view,
|
||||
Transaction *transaction, Indices *indices,
|
||||
Config::Items config)
|
||||
Constraints *constraints, Config::Items config)
|
||||
: index_accessor_(std::move(index_accessor)),
|
||||
label_(label),
|
||||
view_(view),
|
||||
transaction_(transaction),
|
||||
indices_(indices),
|
||||
constraints_(constraints),
|
||||
config_(config) {}
|
||||
|
||||
bool LabelPropertyIndex::Entry::operator<(const Entry &rhs) {
|
||||
@ -484,7 +486,8 @@ LabelPropertyIndex::Iterable::Iterator::Iterator(
|
||||
Iterable *self, utils::SkipList<Entry>::Iterator index_iterator)
|
||||
: self_(self),
|
||||
index_iterator_(index_iterator),
|
||||
current_vertex_accessor_(nullptr, nullptr, nullptr, self_->config_),
|
||||
current_vertex_accessor_(nullptr, nullptr, nullptr, nullptr,
|
||||
self_->config_),
|
||||
current_vertex_(nullptr) {
|
||||
AdvanceUntilValid();
|
||||
}
|
||||
@ -529,7 +532,7 @@ void LabelPropertyIndex::Iterable::Iterator::AdvanceUntilValid() {
|
||||
current_vertex_ = index_iterator_->vertex;
|
||||
current_vertex_accessor_ =
|
||||
VertexAccessor(current_vertex_, self_->transaction_, self_->indices_,
|
||||
self_->config_);
|
||||
self_->constraints_, self_->config_);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -553,7 +556,8 @@ LabelPropertyIndex::Iterable::Iterable(
|
||||
PropertyId property,
|
||||
const std::optional<utils::Bound<PropertyValue>> &lower_bound,
|
||||
const std::optional<utils::Bound<PropertyValue>> &upper_bound, View view,
|
||||
Transaction *transaction, Indices *indices, Config::Items config)
|
||||
Transaction *transaction, Indices *indices, Constraints *constraints,
|
||||
Config::Items config)
|
||||
: index_accessor_(std::move(index_accessor)),
|
||||
label_(label),
|
||||
property_(property),
|
||||
@ -562,6 +566,7 @@ LabelPropertyIndex::Iterable::Iterable(
|
||||
view_(view),
|
||||
transaction_(transaction),
|
||||
indices_(indices),
|
||||
constraints_(constraints),
|
||||
config_(config) {
|
||||
// We have to fix the bounds that the user provided to us. If the user
|
||||
// provided only one bound we should make sure that only values of that type
|
||||
|
@ -14,6 +14,7 @@
|
||||
namespace storage {
|
||||
|
||||
struct Indices;
|
||||
struct Constraints;
|
||||
|
||||
class LabelIndex {
|
||||
private:
|
||||
@ -41,8 +42,8 @@ class LabelIndex {
|
||||
};
|
||||
|
||||
public:
|
||||
LabelIndex(Indices *indices, Config::Items config)
|
||||
: indices_(indices), config_(config) {}
|
||||
LabelIndex(Indices *indices, Constraints *constraints, Config::Items config)
|
||||
: indices_(indices), constraints_(constraints), config_(config) {}
|
||||
|
||||
/// @throw std::bad_alloc
|
||||
void UpdateOnAddLabel(LabelId label, Vertex *vertex, const Transaction &tx);
|
||||
@ -64,7 +65,7 @@ class LabelIndex {
|
||||
public:
|
||||
Iterable(utils::SkipList<Entry>::Accessor index_accessor, LabelId label,
|
||||
View view, Transaction *transaction, Indices *indices,
|
||||
Config::Items config);
|
||||
Constraints *constraints, Config::Items config);
|
||||
|
||||
class Iterator {
|
||||
public:
|
||||
@ -99,6 +100,7 @@ class LabelIndex {
|
||||
View view_;
|
||||
Transaction *transaction_;
|
||||
Indices *indices_;
|
||||
Constraints *constraints_;
|
||||
Config::Items config_;
|
||||
};
|
||||
|
||||
@ -108,7 +110,7 @@ class LabelIndex {
|
||||
CHECK(it != index_.end())
|
||||
<< "Index for label " << label.AsUint() << " doesn't exist";
|
||||
return Iterable(it->second.access(), label, view, transaction, indices_,
|
||||
config_);
|
||||
constraints_, config_);
|
||||
}
|
||||
|
||||
int64_t ApproximateVertexCount(LabelId label) {
|
||||
@ -123,6 +125,7 @@ class LabelIndex {
|
||||
private:
|
||||
std::map<LabelId, utils::SkipList<Entry>> index_;
|
||||
Indices *indices_;
|
||||
Constraints *constraints_;
|
||||
Config::Items config_;
|
||||
};
|
||||
|
||||
@ -141,8 +144,8 @@ class LabelPropertyIndex {
|
||||
};
|
||||
|
||||
public:
|
||||
LabelPropertyIndex(Indices *indices, Config::Items config)
|
||||
: indices_(indices), config_(config) {}
|
||||
LabelPropertyIndex(Indices *indices, Constraints *constraints, Config::Items config)
|
||||
: indices_(indices), constraints_(constraints), config_(config) {}
|
||||
|
||||
/// @throw std::bad_alloc
|
||||
void UpdateOnAddLabel(LabelId label, Vertex *vertex, const Transaction &tx);
|
||||
@ -174,7 +177,7 @@ class LabelPropertyIndex {
|
||||
const std::optional<utils::Bound<PropertyValue>> &lower_bound,
|
||||
const std::optional<utils::Bound<PropertyValue>> &upper_bound,
|
||||
View view, Transaction *transaction, Indices *indices,
|
||||
Config::Items config);
|
||||
Constraints *constraints, Config::Items config);
|
||||
|
||||
class Iterator {
|
||||
public:
|
||||
@ -213,6 +216,7 @@ class LabelPropertyIndex {
|
||||
View view_;
|
||||
Transaction *transaction_;
|
||||
Indices *indices_;
|
||||
Constraints *constraints_;
|
||||
Config::Items config_;
|
||||
};
|
||||
|
||||
@ -226,7 +230,8 @@ class LabelPropertyIndex {
|
||||
<< "Index for label " << label.AsUint() << " and property "
|
||||
<< property.AsUint() << " doesn't exist";
|
||||
return Iterable(it->second.access(), label, property, lower_bound,
|
||||
upper_bound, view, transaction, indices_, config_);
|
||||
upper_bound, view, transaction, indices_, constraints_,
|
||||
config_);
|
||||
}
|
||||
|
||||
int64_t ApproximateVertexCount(LabelId label, PropertyId property) const {
|
||||
@ -250,12 +255,14 @@ class LabelPropertyIndex {
|
||||
private:
|
||||
std::map<std::pair<LabelId, PropertyId>, utils::SkipList<Entry>> index_;
|
||||
Indices *indices_;
|
||||
Constraints *constraints_;
|
||||
Config::Items config_;
|
||||
};
|
||||
|
||||
struct Indices {
|
||||
explicit Indices(Config::Items config)
|
||||
: label_index(this, config), label_property_index(this, config) {}
|
||||
Indices(Constraints *constraints, Config::Items config)
|
||||
: label_index(this, constraints, config),
|
||||
label_property_index(this, constraints, config) {}
|
||||
|
||||
// Disable copy and move because members hold pointer to `this`.
|
||||
Indices(const Indices &) = delete;
|
||||
|
@ -15,9 +15,9 @@ auto AdvanceToVisibleVertex(utils::SkipList<Vertex>::Iterator it,
|
||||
utils::SkipList<Vertex>::Iterator end,
|
||||
std::optional<VertexAccessor> *vertex,
|
||||
Transaction *tx, View view, Indices *indices,
|
||||
Config::Items config) {
|
||||
Constraints *constraints, Config::Items config) {
|
||||
while (it != end) {
|
||||
*vertex = VertexAccessor::Create(&*it, tx, indices, config, view);
|
||||
*vertex = VertexAccessor::Create(&*it, tx, indices, constraints, config, view);
|
||||
if (!*vertex) {
|
||||
++it;
|
||||
continue;
|
||||
@ -32,7 +32,8 @@ AllVerticesIterable::Iterator::Iterator(AllVerticesIterable *self,
|
||||
: self_(self),
|
||||
it_(AdvanceToVisibleVertex(it, self->vertices_accessor_.end(),
|
||||
&self->vertex_, self->transaction_,
|
||||
self->view_, self->indices_, self->config_)) {}
|
||||
self->view_, self->indices_,
|
||||
self_->constraints_, self->config_)) {}
|
||||
|
||||
VertexAccessor AllVerticesIterable::Iterator::operator*() const {
|
||||
return *self_->vertex_;
|
||||
@ -42,7 +43,8 @@ AllVerticesIterable::Iterator &AllVerticesIterable::Iterator::operator++() {
|
||||
++it_;
|
||||
it_ = AdvanceToVisibleVertex(it_, self_->vertices_accessor_.end(),
|
||||
&self_->vertex_, self_->transaction_,
|
||||
self_->view_, self_->indices_, self_->config_);
|
||||
self_->view_, self_->indices_,
|
||||
self_->constraints_, self_->config_);
|
||||
return *this;
|
||||
}
|
||||
|
||||
@ -288,7 +290,7 @@ bool VerticesIterable::Iterator::operator==(const Iterator &other) const {
|
||||
}
|
||||
|
||||
Storage::Storage(Config config)
|
||||
: indices_(config.items),
|
||||
: indices_(&constraints_, config.items),
|
||||
config_(config),
|
||||
durability_(config.durability, &vertices_, &edges_, &name_id_mapper_,
|
||||
&edge_count_, &indices_, &constraints_, config.items) {
|
||||
@ -356,7 +358,8 @@ VertexAccessor Storage::Accessor::CreateVertex() {
|
||||
CHECK(inserted) << "The vertex must be inserted here!";
|
||||
CHECK(it != acc.end()) << "Invalid Vertex accessor!";
|
||||
delta->prev.Set(&*it);
|
||||
return VertexAccessor(&*it, &transaction_, &storage_->indices_, config_);
|
||||
return VertexAccessor(&*it, &transaction_, &storage_->indices_,
|
||||
&storage_->constraints_, config_);
|
||||
}
|
||||
|
||||
std::optional<VertexAccessor> Storage::Accessor::FindVertex(Gid gid,
|
||||
@ -365,7 +368,7 @@ std::optional<VertexAccessor> Storage::Accessor::FindVertex(Gid gid,
|
||||
auto it = acc.find(gid);
|
||||
if (it == acc.end()) return std::nullopt;
|
||||
return VertexAccessor::Create(&*it, &transaction_, &storage_->indices_,
|
||||
config_, view);
|
||||
&storage_->constraints_, config_, view);
|
||||
}
|
||||
|
||||
Result<bool> Storage::Accessor::DeleteVertex(VertexAccessor *vertex) {
|
||||
@ -414,7 +417,7 @@ Result<bool> Storage::Accessor::DetachDeleteVertex(VertexAccessor *vertex) {
|
||||
for (const auto &item : in_edges) {
|
||||
auto [edge_type, from_vertex, edge] = item;
|
||||
EdgeAccessor e(edge, edge_type, from_vertex, vertex_ptr, &transaction_,
|
||||
&storage_->indices_, config_);
|
||||
&storage_->indices_, &storage_->constraints_, config_);
|
||||
auto ret = DeleteEdge(&e);
|
||||
if (ret.HasError()) {
|
||||
CHECK(ret.GetError() == Error::SERIALIZATION_ERROR)
|
||||
@ -425,7 +428,7 @@ Result<bool> Storage::Accessor::DetachDeleteVertex(VertexAccessor *vertex) {
|
||||
for (const auto &item : out_edges) {
|
||||
auto [edge_type, to_vertex, edge] = item;
|
||||
EdgeAccessor e(edge, edge_type, vertex_ptr, to_vertex, &transaction_,
|
||||
&storage_->indices_, config_);
|
||||
&storage_->indices_, &storage_->constraints_, config_);
|
||||
auto ret = DeleteEdge(&e);
|
||||
if (ret.HasError()) {
|
||||
CHECK(ret.GetError() == Error::SERIALIZATION_ERROR)
|
||||
@ -514,7 +517,7 @@ Result<EdgeAccessor> Storage::Accessor::CreateEdge(VertexAccessor *from,
|
||||
storage_->edge_count_.fetch_add(1, std::memory_order_acq_rel);
|
||||
|
||||
return EdgeAccessor(edge, edge_type, from_vertex, to_vertex, &transaction_,
|
||||
&storage_->indices_, config_);
|
||||
&storage_->indices_, &storage_->constraints_, config_);
|
||||
}
|
||||
|
||||
Result<bool> Storage::Accessor::DeleteEdge(EdgeAccessor *edge) {
|
||||
@ -635,7 +638,7 @@ EdgeTypeId Storage::Accessor::NameToEdgeType(const std::string &name) {
|
||||
|
||||
void Storage::Accessor::AdvanceCommand() { ++transaction_.command_id; }
|
||||
|
||||
utils::BasicResult<ExistenceConstraintViolation, void>
|
||||
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!";
|
||||
@ -662,6 +665,11 @@ Storage::Accessor::Commit() {
|
||||
}
|
||||
}
|
||||
|
||||
// Result of validating the vertex against unqiue constraints. It has to be
|
||||
// declared outside of the critical section scope because its value is
|
||||
// tested for Abort call which has to be done out of the scope.
|
||||
std::optional<ConstraintViolation> unique_constraint_violation;
|
||||
|
||||
// Save these so we can mark them used in the commit log.
|
||||
uint64_t start_timestamp = transaction_.start_timestamp;
|
||||
uint64_t commit_timestamp;
|
||||
@ -670,35 +678,72 @@ Storage::Accessor::Commit() {
|
||||
std::unique_lock<utils::SpinLock> engine_guard(storage_->engine_lock_);
|
||||
commit_timestamp = storage_->timestamp_++;
|
||||
|
||||
// Write transaction to WAL while holding the engine lock to make sure
|
||||
// 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
|
||||
// modifications before they are written to disk.
|
||||
storage_->durability_.AppendToWal(transaction_, commit_timestamp);
|
||||
// Before committing and validating vertices against unique constraints,
|
||||
// we have to update unique constraints with the vertices that are going
|
||||
// to be validated/committed.
|
||||
for (const auto &delta : transaction_.deltas) {
|
||||
auto prev = delta.prev.Get();
|
||||
if (prev.type != PreviousPtr::Type::VERTEX) {
|
||||
continue;
|
||||
}
|
||||
storage_->constraints_.unique_constraints.UpdateBeforeCommit(
|
||||
prev.vertex, transaction_);
|
||||
}
|
||||
|
||||
// Take committed_transactions lock while holding the engine lock to
|
||||
// make sure that committed transactions are sorted by the commit
|
||||
// timestamp in the list.
|
||||
storage_->committed_transactions_.WithLock(
|
||||
[&](auto &committed_transactions) {
|
||||
// TODO: release lock, and update all deltas to have a local copy
|
||||
// of the commit timestamp
|
||||
CHECK(transaction_.commit_timestamp != nullptr)
|
||||
<< "Invalid database state!";
|
||||
transaction_.commit_timestamp->store(commit_timestamp,
|
||||
// Validate that unique constraints are satisfied for all modified
|
||||
// vertices.
|
||||
for (const auto &delta : transaction_.deltas) {
|
||||
auto prev = delta.prev.Get();
|
||||
if (prev.type != PreviousPtr::Type::VERTEX) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (unique_constraint_violation) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!unique_constraint_violation) {
|
||||
// Write transaction to WAL while holding the engine lock to make sure
|
||||
// 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
|
||||
// modifications before they are written to disk.
|
||||
storage_->durability_.AppendToWal(transaction_, commit_timestamp);
|
||||
|
||||
// Take committed_transactions lock while holding the engine lock to
|
||||
// make sure that committed transactions are sorted by the commit
|
||||
// timestamp in the list.
|
||||
storage_->committed_transactions_.WithLock(
|
||||
[&](auto &committed_transactions) {
|
||||
// TODO: release lock, and update all deltas to have a local copy
|
||||
// of the commit timestamp
|
||||
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.
|
||||
engine_guard.unlock();
|
||||
committed_transactions.emplace_back(std::move(transaction_));
|
||||
});
|
||||
// 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_));
|
||||
});
|
||||
|
||||
storage_->commit_log_.MarkFinished(start_timestamp);
|
||||
storage_->commit_log_.MarkFinished(commit_timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
storage_->commit_log_.MarkFinished(start_timestamp);
|
||||
storage_->commit_log_.MarkFinished(commit_timestamp);
|
||||
if (unique_constraint_violation) {
|
||||
Abort();
|
||||
storage_->commit_log_.MarkFinished(commit_timestamp);
|
||||
return *unique_constraint_violation;
|
||||
}
|
||||
}
|
||||
is_transaction_active_ = false;
|
||||
|
||||
@ -987,7 +1032,7 @@ IndicesInfo Storage::ListAllIndices() const {
|
||||
indices_.label_property_index.ListIndices()};
|
||||
}
|
||||
|
||||
utils::BasicResult<ExistenceConstraintViolation, bool>
|
||||
utils::BasicResult<ConstraintViolation, bool>
|
||||
Storage::CreateExistenceConstraint(LabelId label, PropertyId property) {
|
||||
std::unique_lock<utils::RWLock> storage_guard(main_lock_);
|
||||
auto ret = ::storage::CreateExistenceConstraint(&constraints_, label,
|
||||
@ -1011,9 +1056,26 @@ bool Storage::DropExistenceConstraint(LabelId label, PropertyId property) {
|
||||
return true;
|
||||
}
|
||||
|
||||
utils::BasicResult<ConstraintViolation, bool>
|
||||
Storage::CreateUniqueConstraint(LabelId label, PropertyId property) {
|
||||
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;
|
||||
}
|
||||
|
||||
bool Storage::DropUniqueConstraint(LabelId label, PropertyId property) {
|
||||
std::unique_lock<utils::RWLock> storage_guard(main_lock_);
|
||||
// TODO(tsabolcec): Append action to the WAL.
|
||||
return constraints_.unique_constraints.DropConstraint(label, property);
|
||||
}
|
||||
|
||||
ConstraintsInfo Storage::ListAllConstraints() const {
|
||||
std::shared_lock<utils::RWLock> storage_guard_(main_lock_);
|
||||
return {ListExistenceConstraints(constraints_)};
|
||||
return {ListExistenceConstraints(constraints_),
|
||||
constraints_.unique_constraints.ListConstraints()};
|
||||
}
|
||||
|
||||
StorageInfo Storage::GetInfo() const {
|
||||
@ -1239,6 +1301,8 @@ void Storage::CollectGarbage() {
|
||||
// This operation is very expensive as it traverses through all of the items
|
||||
// in every index every time.
|
||||
RemoveObsoleteEntries(&indices_, oldest_active_start_timestamp);
|
||||
constraints_.unique_constraints.RemoveObsoleteEntries(
|
||||
oldest_active_start_timestamp);
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -37,6 +37,7 @@ class AllVerticesIterable final {
|
||||
Transaction *transaction_;
|
||||
View view_;
|
||||
Indices *indices_;
|
||||
Constraints *constraints_;
|
||||
Config::Items config_;
|
||||
std::optional<VertexAccessor> vertex_;
|
||||
|
||||
@ -61,11 +62,12 @@ class AllVerticesIterable final {
|
||||
|
||||
AllVerticesIterable(utils::SkipList<Vertex>::Accessor vertices_accessor,
|
||||
Transaction *transaction, View view, Indices *indices,
|
||||
Config::Items config)
|
||||
Constraints *constraints, Config::Items config)
|
||||
: vertices_accessor_(std::move(vertices_accessor)),
|
||||
transaction_(transaction),
|
||||
view_(view),
|
||||
indices_(indices),
|
||||
constraints_(constraints),
|
||||
config_(config) {}
|
||||
|
||||
Iterator begin() { return Iterator(this, vertices_accessor_.begin()); }
|
||||
@ -144,6 +146,7 @@ struct IndicesInfo {
|
||||
/// storage.
|
||||
struct ConstraintsInfo {
|
||||
std::vector<std::pair<LabelId, PropertyId>> existence;
|
||||
std::vector<std::pair<LabelId, PropertyId>> unique;
|
||||
};
|
||||
|
||||
/// Structure used to return information about the storage.
|
||||
@ -188,7 +191,8 @@ class Storage final {
|
||||
VerticesIterable Vertices(View view) {
|
||||
return VerticesIterable(
|
||||
AllVerticesIterable(storage_->vertices_.access(), &transaction_, view,
|
||||
&storage_->indices_, storage_->config_.items));
|
||||
&storage_->indices_, &storage_->constraints_,
|
||||
storage_->config_.items));
|
||||
}
|
||||
|
||||
VerticesIterable Vertices(LabelId label, View view);
|
||||
@ -284,16 +288,17 @@ class Storage final {
|
||||
}
|
||||
|
||||
ConstraintsInfo ListAllConstraints() const {
|
||||
return {ListExistenceConstraints(storage_->constraints_)};
|
||||
return {ListExistenceConstraints(storage_->constraints_),
|
||||
storage_->constraints_.unique_constraints.ListConstraints()};
|
||||
}
|
||||
|
||||
void AdvanceCommand();
|
||||
|
||||
/// Commit returns `ExistenceConstraintViolation` if the changes made by
|
||||
/// this transaction violate an existence constraint. In that case the
|
||||
/// Commit returns `ConstraintViolation` if the changes made by this
|
||||
/// transaction violate an existence or unique constraint. In that case the
|
||||
/// transaction is automatically aborted. Otherwise, void is returned.
|
||||
/// @throw std::bad_alloc
|
||||
utils::BasicResult<ExistenceConstraintViolation, void> Commit();
|
||||
utils::BasicResult<ConstraintViolation, void> Commit();
|
||||
|
||||
/// @throw std::bad_alloc
|
||||
void Abort();
|
||||
@ -333,19 +338,30 @@ class Storage final {
|
||||
|
||||
IndicesInfo ListAllIndices() const;
|
||||
|
||||
/// Creates a unique constraint`. Returns true if the constraint was
|
||||
/// successfuly added, false if it already exists and an
|
||||
/// `ExistenceConstraintViolation` if there is an existing vertex violating
|
||||
/// the constraint.
|
||||
/// Creates an existence constraint. Returns true if the constraint was
|
||||
/// successfuly 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
|
||||
utils::BasicResult<ExistenceConstraintViolation, bool>
|
||||
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.
|
||||
///
|
||||
/// @throw std::bad_alloc
|
||||
utils::BasicResult<ConstraintViolation, bool>
|
||||
CreateUniqueConstraint(LabelId label, PropertyId property);
|
||||
|
||||
/// Removes a unique constraint. Returns true if the constraint was removed,
|
||||
/// and false if it doesn't exist.
|
||||
bool DropExistenceConstraint(LabelId label, PropertyId property);
|
||||
bool DropUniqueConstraint(LabelId label, PropertyId property);
|
||||
|
||||
ConstraintsInfo ListAllConstraints() const;
|
||||
|
||||
@ -378,8 +394,8 @@ class Storage final {
|
||||
|
||||
NameIdMapper name_id_mapper_;
|
||||
|
||||
Indices indices_;
|
||||
Constraints constraints_;
|
||||
Indices indices_;
|
||||
|
||||
// Transaction engine
|
||||
utils::SpinLock engine_lock_;
|
||||
|
@ -12,6 +12,7 @@ namespace storage {
|
||||
std::optional<VertexAccessor> VertexAccessor::Create(Vertex *vertex,
|
||||
Transaction *transaction,
|
||||
Indices *indices,
|
||||
Constraints *constraints,
|
||||
Config::Items config,
|
||||
View view) {
|
||||
bool is_visible = true;
|
||||
@ -43,7 +44,7 @@ std::optional<VertexAccessor> VertexAccessor::Create(Vertex *vertex,
|
||||
}
|
||||
});
|
||||
if (!is_visible) return std::nullopt;
|
||||
return VertexAccessor{vertex, transaction, indices, config};
|
||||
return VertexAccessor{vertex, transaction, indices, constraints, config};
|
||||
}
|
||||
|
||||
Result<bool> VertexAccessor::AddLabel(LabelId label) {
|
||||
@ -435,7 +436,7 @@ Result<std::vector<EdgeAccessor>> VertexAccessor::InEdges(
|
||||
for (const auto &item : in_edges) {
|
||||
const auto &[edge_type, from_vertex, edge] = item;
|
||||
ret.emplace_back(edge, edge_type, from_vertex, vertex_, transaction_,
|
||||
indices_, config_);
|
||||
indices_, constraints_, config_);
|
||||
}
|
||||
return std::move(ret);
|
||||
}
|
||||
@ -528,7 +529,7 @@ Result<std::vector<EdgeAccessor>> VertexAccessor::OutEdges(
|
||||
for (const auto &item : out_edges) {
|
||||
const auto &[edge_type, to_vertex, edge] = item;
|
||||
ret.emplace_back(edge, edge_type, vertex_, to_vertex, transaction_,
|
||||
indices_, config_);
|
||||
indices_, constraints_, config_);
|
||||
}
|
||||
return std::move(ret);
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ namespace storage {
|
||||
class EdgeAccessor;
|
||||
class Storage;
|
||||
struct Indices;
|
||||
struct Constraints;
|
||||
|
||||
class VertexAccessor final {
|
||||
private:
|
||||
@ -21,15 +22,17 @@ class VertexAccessor final {
|
||||
|
||||
public:
|
||||
VertexAccessor(Vertex *vertex, Transaction *transaction, Indices *indices,
|
||||
Config::Items config)
|
||||
Constraints *constraints, Config::Items config)
|
||||
: vertex_(vertex),
|
||||
transaction_(transaction),
|
||||
indices_(indices),
|
||||
constraints_(constraints),
|
||||
config_(config) {}
|
||||
|
||||
static std::optional<VertexAccessor> Create(Vertex *vertex,
|
||||
Transaction *transaction,
|
||||
Indices *indices,
|
||||
Constraints *constraints,
|
||||
Config::Items config, View view);
|
||||
|
||||
/// Add a label and return `true` if insertion took place.
|
||||
@ -96,6 +99,7 @@ class VertexAccessor final {
|
||||
Vertex *vertex_;
|
||||
Transaction *transaction_;
|
||||
Indices *indices_;
|
||||
Constraints *constraints_;
|
||||
Config::Items config_;
|
||||
};
|
||||
|
||||
|
@ -11,9 +11,10 @@ using testing::UnorderedElementsAre;
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||
#define ASSERT_NO_ERROR(result) ASSERT_FALSE((result).HasError())
|
||||
|
||||
bool operator==(const ExistenceConstraintViolation &lhs,
|
||||
const ExistenceConstraintViolation &rhs) {
|
||||
return lhs.label == rhs.label && lhs.property == rhs.property;
|
||||
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 {
|
||||
@ -32,7 +33,7 @@ class ConstraintsTest : public testing::Test {
|
||||
};
|
||||
|
||||
// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
TEST_F(ConstraintsTest, CreateAndDrop) {
|
||||
TEST_F(ConstraintsTest, ExistenceConstraintsCreateAndDrop) {
|
||||
EXPECT_EQ(storage.ListAllConstraints().existence.size(), 0);
|
||||
{
|
||||
auto res = storage.CreateExistenceConstraint(label1, prop1);
|
||||
@ -69,7 +70,7 @@ TEST_F(ConstraintsTest, CreateAndDrop) {
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
TEST_F(ConstraintsTest, CreateFailure1) {
|
||||
TEST_F(ConstraintsTest, ExistenceConstraintsCreateFailure1) {
|
||||
{
|
||||
auto acc = storage.Access();
|
||||
auto vertex = acc.CreateVertex();
|
||||
@ -78,9 +79,10 @@ TEST_F(ConstraintsTest, CreateFailure1) {
|
||||
}
|
||||
{
|
||||
auto res = storage.CreateExistenceConstraint(label1, prop1);
|
||||
EXPECT_TRUE(
|
||||
res.HasError() &&
|
||||
(res.GetError() == ExistenceConstraintViolation{label1, prop1}));
|
||||
ASSERT_TRUE(res.HasError());
|
||||
EXPECT_EQ(res.GetError(),
|
||||
(ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label1,
|
||||
prop1}));
|
||||
}
|
||||
{
|
||||
auto acc = storage.Access();
|
||||
@ -96,7 +98,7 @@ TEST_F(ConstraintsTest, CreateFailure1) {
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
TEST_F(ConstraintsTest, CreateFailure2) {
|
||||
TEST_F(ConstraintsTest, ExistenceConstraintsCreateFailure2) {
|
||||
{
|
||||
auto acc = storage.Access();
|
||||
auto vertex = acc.CreateVertex();
|
||||
@ -105,9 +107,10 @@ TEST_F(ConstraintsTest, CreateFailure2) {
|
||||
}
|
||||
{
|
||||
auto res = storage.CreateExistenceConstraint(label1, prop1);
|
||||
EXPECT_TRUE(
|
||||
res.HasError() &&
|
||||
(res.GetError() == ExistenceConstraintViolation{label1, prop1}));
|
||||
ASSERT_TRUE(res.HasError());
|
||||
EXPECT_EQ(res.GetError(),
|
||||
(ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label1,
|
||||
prop1}));
|
||||
}
|
||||
{
|
||||
auto acc = storage.Access();
|
||||
@ -123,7 +126,7 @@ TEST_F(ConstraintsTest, CreateFailure2) {
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
TEST_F(ConstraintsTest, ViolationOnCommit) {
|
||||
TEST_F(ConstraintsTest, ExistenceConstraintsViolationOnCommit) {
|
||||
{
|
||||
auto res = storage.CreateExistenceConstraint(label1, prop1);
|
||||
ASSERT_TRUE(res.HasValue() && res.GetValue());
|
||||
@ -135,9 +138,10 @@ TEST_F(ConstraintsTest, ViolationOnCommit) {
|
||||
ASSERT_NO_ERROR(vertex.AddLabel(label1));
|
||||
|
||||
auto res = acc.Commit();
|
||||
EXPECT_TRUE(
|
||||
res.HasError() &&
|
||||
(res.GetError() == ExistenceConstraintViolation{label1, prop1}));
|
||||
ASSERT_TRUE(res.HasError());
|
||||
EXPECT_EQ(res.GetError(),
|
||||
(ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label1,
|
||||
prop1}));
|
||||
}
|
||||
|
||||
{
|
||||
@ -155,9 +159,10 @@ TEST_F(ConstraintsTest, ViolationOnCommit) {
|
||||
}
|
||||
|
||||
auto res = acc.Commit();
|
||||
EXPECT_TRUE(
|
||||
res.HasError() &&
|
||||
(res.GetError() == ExistenceConstraintViolation{label1, prop1}));
|
||||
ASSERT_TRUE(res.HasError());
|
||||
EXPECT_EQ(res.GetError(),
|
||||
(ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label1,
|
||||
prop1}));
|
||||
}
|
||||
|
||||
{
|
||||
@ -181,3 +186,439 @@ TEST_F(ConstraintsTest, ViolationOnCommit) {
|
||||
ASSERT_NO_ERROR(acc.Commit());
|
||||
}
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
TEST_F(ConstraintsTest, UniqueConstraintsCreateAndDrop) {
|
||||
EXPECT_EQ(storage.ListAllConstraints().unique.size(), 0);
|
||||
{
|
||||
auto res = storage.CreateUniqueConstraint(label1, prop1);
|
||||
EXPECT_TRUE(res.HasValue() && res.GetValue());
|
||||
}
|
||||
EXPECT_THAT(storage.ListAllConstraints().unique,
|
||||
UnorderedElementsAre(std::make_pair(label1, prop1)));
|
||||
{
|
||||
auto res = storage.CreateUniqueConstraint(label1, prop1);
|
||||
EXPECT_TRUE(res.HasValue() && !res.GetValue());
|
||||
}
|
||||
EXPECT_THAT(storage.ListAllConstraints().unique,
|
||||
UnorderedElementsAre(std::make_pair(label1, prop1)));
|
||||
{
|
||||
auto res = storage.CreateUniqueConstraint(label2, prop1);
|
||||
EXPECT_TRUE(res.HasValue() && res.GetValue());
|
||||
}
|
||||
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));
|
||||
EXPECT_THAT(storage.ListAllConstraints().unique,
|
||||
UnorderedElementsAre(std::make_pair(label2, 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());
|
||||
}
|
||||
EXPECT_THAT(storage.ListAllConstraints().unique,
|
||||
UnorderedElementsAre(std::make_pair(label2, prop1)));
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
TEST_F(ConstraintsTest, UniqueConstraintsCreateFailure1) {
|
||||
{
|
||||
auto acc = storage.Access();
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
auto vertex1 = acc.CreateVertex();
|
||||
ASSERT_NO_ERROR(vertex1.AddLabel(label1));
|
||||
ASSERT_NO_ERROR(vertex1.SetProperty(prop1, PropertyValue(1)));
|
||||
}
|
||||
ASSERT_NO_ERROR(acc.Commit());
|
||||
}
|
||||
|
||||
{
|
||||
auto res = storage.CreateUniqueConstraint(label1, prop1);
|
||||
ASSERT_TRUE(res.HasError());
|
||||
EXPECT_EQ(res.GetError(),
|
||||
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
|
||||
prop1}));
|
||||
}
|
||||
|
||||
{
|
||||
auto acc = storage.Access();
|
||||
for (auto vertex : acc.Vertices(View::OLD)) {
|
||||
ASSERT_NO_ERROR(acc.DeleteVertex(&vertex));
|
||||
}
|
||||
ASSERT_NO_ERROR(acc.Commit());
|
||||
}
|
||||
|
||||
{
|
||||
auto res = storage.CreateUniqueConstraint(label1, prop1);
|
||||
EXPECT_TRUE(!res.HasError() && res.GetValue());
|
||||
}
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
TEST_F(ConstraintsTest, UniqueConstraintsCreateFailure2) {
|
||||
{
|
||||
auto acc = storage.Access();
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
auto vertex = acc.CreateVertex();
|
||||
ASSERT_NO_ERROR(vertex.AddLabel(label1));
|
||||
ASSERT_NO_ERROR(vertex.SetProperty(prop1, PropertyValue(1)));
|
||||
}
|
||||
ASSERT_NO_ERROR(acc.Commit());
|
||||
}
|
||||
|
||||
{
|
||||
auto res = storage.CreateUniqueConstraint(label1, prop1);
|
||||
ASSERT_TRUE(res.HasError());
|
||||
EXPECT_EQ(res.GetError(),
|
||||
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
|
||||
prop1}));
|
||||
}
|
||||
|
||||
{
|
||||
auto acc = storage.Access();
|
||||
int value = 0;
|
||||
for (auto vertex : acc.Vertices(View::OLD)) {
|
||||
ASSERT_NO_ERROR(vertex.SetProperty(prop1, PropertyValue(value)));
|
||||
++value;
|
||||
}
|
||||
ASSERT_NO_ERROR(acc.Commit());
|
||||
}
|
||||
|
||||
{
|
||||
auto res = storage.CreateUniqueConstraint(label1, prop1);
|
||||
EXPECT_TRUE(!res.HasError() && res.GetValue());
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
{
|
||||
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(vertex2.AddLabel(label1));
|
||||
ASSERT_NO_ERROR(vertex2.SetProperty(prop1, PropertyValue(2)));
|
||||
ASSERT_NO_ERROR(acc.Commit());
|
||||
}
|
||||
|
||||
{
|
||||
auto acc = storage.Access();
|
||||
auto vertex1 = acc.FindVertex(gid1, View::OLD);
|
||||
auto vertex2 = acc.FindVertex(gid2, View::OLD);
|
||||
ASSERT_NO_ERROR(vertex1->SetProperty(prop1, PropertyValue(2)));
|
||||
ASSERT_NO_ERROR(vertex2->SetProperty(prop1, PropertyValue(1)));
|
||||
ASSERT_NO_ERROR(acc.Commit());
|
||||
}
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
TEST_F(ConstraintsTest, UniqueConstraintsNoViolation2) {
|
||||
{
|
||||
auto res = storage.CreateUniqueConstraint(label1, prop1);
|
||||
ASSERT_TRUE(res.HasValue() && res.GetValue());
|
||||
}
|
||||
|
||||
{
|
||||
// tx1: B---SP(v1, 1)---SP(v1, 2)---OK--
|
||||
// tx2: -B---SP(v2, 2)---SP(v2, 1)---OK-
|
||||
|
||||
auto acc1 = storage.Access();
|
||||
auto acc2 = storage.Access();
|
||||
auto vertex1 = acc1.CreateVertex();
|
||||
auto vertex2 = acc2.CreateVertex();
|
||||
|
||||
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(vertex1.SetProperty(prop1, PropertyValue(2)));
|
||||
ASSERT_NO_ERROR(vertex2.SetProperty(prop1, PropertyValue(1)));
|
||||
|
||||
ASSERT_NO_ERROR(acc1.Commit());
|
||||
ASSERT_NO_ERROR(acc2.Commit());
|
||||
}
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
TEST_F(ConstraintsTest, UniqueConstraintsNoViolation3) {
|
||||
{
|
||||
auto res = storage.CreateUniqueConstraint(label1, prop1);
|
||||
ASSERT_TRUE(res.HasValue() && res.GetValue());
|
||||
}
|
||||
|
||||
{
|
||||
// tx1: B---SP(v1, 1)---OK----------------------
|
||||
// tx2: --------------------B---SP(v1, 2)---OK--
|
||||
// tx3: ---------------------B---SP(v2, 1)---OK-
|
||||
|
||||
auto acc1 = storage.Access();
|
||||
auto vertex1 = acc1.CreateVertex();
|
||||
auto gid = vertex1.Gid();
|
||||
|
||||
ASSERT_NO_ERROR(vertex1.AddLabel(label1));
|
||||
ASSERT_NO_ERROR(vertex1.SetProperty(prop1, PropertyValue(1)));
|
||||
|
||||
ASSERT_NO_ERROR(acc1.Commit());
|
||||
|
||||
auto acc2 = storage.Access();
|
||||
auto acc3 = storage.Access();
|
||||
auto vertex2 = acc2.FindVertex(gid, View::NEW); // vertex1 == vertex2
|
||||
auto vertex3 = acc3.CreateVertex();
|
||||
|
||||
ASSERT_NO_ERROR(vertex2->SetProperty(prop1, PropertyValue(2)));
|
||||
ASSERT_NO_ERROR(vertex3.AddLabel(label1));
|
||||
ASSERT_NO_ERROR(vertex3.SetProperty(prop1, PropertyValue(1)));
|
||||
|
||||
ASSERT_NO_ERROR(acc2.Commit());
|
||||
ASSERT_NO_ERROR(acc3.Commit());
|
||||
}
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
TEST_F(ConstraintsTest, UniqueConstraintsNoViolation4) {
|
||||
{
|
||||
auto res = storage.CreateUniqueConstraint(label1, prop1);
|
||||
ASSERT_TRUE(res.HasValue() && res.GetValue());
|
||||
}
|
||||
|
||||
{
|
||||
// tx1: B---SP(v1, 1)---OK-----------------------
|
||||
// tx2: --------------------B---SP(v2, 1)-----OK-
|
||||
// tx3: ---------------------B---SP(v1, 2)---OK--
|
||||
|
||||
auto acc1 = storage.Access();
|
||||
auto vertex1 = acc1.CreateVertex();
|
||||
auto gid = vertex1.Gid();
|
||||
|
||||
ASSERT_NO_ERROR(vertex1.AddLabel(label1));
|
||||
ASSERT_NO_ERROR(vertex1.SetProperty(prop1, PropertyValue(1)));
|
||||
|
||||
ASSERT_NO_ERROR(acc1.Commit());
|
||||
|
||||
auto acc2 = storage.Access();
|
||||
auto acc3 = storage.Access();
|
||||
auto vertex2 = acc2.CreateVertex();
|
||||
auto vertex3 = acc3.FindVertex(gid, View::NEW);
|
||||
|
||||
ASSERT_NO_ERROR(vertex2.AddLabel(label1));
|
||||
ASSERT_NO_ERROR(vertex2.SetProperty(prop1, PropertyValue(1)));
|
||||
ASSERT_NO_ERROR(vertex3->SetProperty(prop1, PropertyValue(2)));
|
||||
|
||||
ASSERT_NO_ERROR(acc3.Commit());
|
||||
ASSERT_NO_ERROR(acc2.Commit());
|
||||
}
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
TEST_F(ConstraintsTest, UniqueConstraintsViolationOnCommit1) {
|
||||
{
|
||||
auto res = storage.CreateUniqueConstraint(label1, prop1);
|
||||
ASSERT_TRUE(res.HasValue() && res.GetValue());
|
||||
}
|
||||
|
||||
{
|
||||
auto acc = storage.Access();
|
||||
auto vertex1 = acc.CreateVertex();
|
||||
auto vertex2 = acc.CreateVertex();
|
||||
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(1)));
|
||||
auto res = acc.Commit();
|
||||
ASSERT_TRUE(res.HasError());
|
||||
EXPECT_EQ(res.GetError(),
|
||||
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
|
||||
prop1}));
|
||||
}
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
TEST_F(ConstraintsTest, UniqueConstraintsViolationOnCommit2) {
|
||||
{
|
||||
auto res = storage.CreateUniqueConstraint(label1, prop1);
|
||||
ASSERT_TRUE(res.HasValue() && res.GetValue());
|
||||
}
|
||||
|
||||
{
|
||||
// tx1: B---SP(v1, 1)---SP(v2, 2)---OK-----------------------
|
||||
// tx2: -------------------------------B---SP(v1, 3)---OK----
|
||||
// tx3: --------------------------------B---SP(v2, 3)---FAIL-
|
||||
|
||||
auto acc1 = storage.Access();
|
||||
auto vertex1 = acc1.CreateVertex();
|
||||
auto vertex2 = acc1.CreateVertex();
|
||||
auto gid1 = vertex1.Gid();
|
||||
auto 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(acc1.Commit());
|
||||
|
||||
auto acc2 = storage.Access();
|
||||
auto acc3 = storage.Access();
|
||||
auto vertex3 = acc2.FindVertex(gid1, View::NEW); // vertex3 == vertex1
|
||||
auto vertex4 = acc3.FindVertex(gid2, View::NEW); // vertex4 == vertex2
|
||||
|
||||
ASSERT_NO_ERROR(vertex3->SetProperty(prop1, PropertyValue(3)));
|
||||
ASSERT_NO_ERROR(vertex4->SetProperty(prop1, PropertyValue(3)));
|
||||
|
||||
ASSERT_NO_ERROR(acc2.Commit());
|
||||
auto res = acc3.Commit();
|
||||
ASSERT_TRUE(res.HasError());
|
||||
EXPECT_EQ(res.GetError(),
|
||||
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
|
||||
prop1}));
|
||||
}
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
TEST_F(ConstraintsTest, UniqueConstraintsViolationOnCommit3) {
|
||||
{
|
||||
auto res = storage.CreateUniqueConstraint(label1, prop1);
|
||||
ASSERT_TRUE(res.HasValue() & res.GetValue());
|
||||
}
|
||||
|
||||
{
|
||||
// tx1: B---SP(v1, 1)---SP(v2, 2)---OK-----------------------
|
||||
// tx2: -------------------------------B---SP(v1, 2)---FAIL--
|
||||
// tx3: --------------------------------B---SP(v2, 1)---FAIL-
|
||||
|
||||
auto acc1 = storage.Access();
|
||||
auto vertex1 = acc1.CreateVertex();
|
||||
auto vertex2 = acc1.CreateVertex();
|
||||
auto gid1 = vertex1.Gid();
|
||||
auto 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(acc1.Commit());
|
||||
|
||||
auto acc2 = storage.Access();
|
||||
auto acc3 = storage.Access();
|
||||
auto vertex3 = acc2.FindVertex(gid1, View::OLD); // vertex3 == vertex1
|
||||
auto vertex4 = acc3.FindVertex(gid2, View::OLD); // vertex4 == vertex2
|
||||
|
||||
ASSERT_NO_ERROR(vertex3->SetProperty(prop1, PropertyValue(2)));
|
||||
ASSERT_NO_ERROR(vertex4->SetProperty(prop1, PropertyValue(1)));
|
||||
|
||||
auto res = acc2.Commit();
|
||||
ASSERT_TRUE(res.HasError());
|
||||
EXPECT_EQ(res.GetError(),
|
||||
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
|
||||
prop1}));
|
||||
res = acc3.Commit();
|
||||
ASSERT_TRUE(res.HasError());
|
||||
EXPECT_EQ(res.GetError(),
|
||||
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
|
||||
prop1}));
|
||||
}
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
TEST_F(ConstraintsTest, UniqueConstraintsLabelAlteration) {
|
||||
{
|
||||
auto res = storage.CreateUniqueConstraint(label1, prop1);
|
||||
ASSERT_TRUE(res.HasValue() && res.GetValue());
|
||||
}
|
||||
|
||||
Gid gid1;
|
||||
Gid gid2;
|
||||
{
|
||||
// B---AL(v2)---SP(v1, 1)---SP(v2, 1)---OK
|
||||
|
||||
auto acc = storage.Access();
|
||||
auto vertex1 = acc.CreateVertex();
|
||||
auto vertex2 = acc.CreateVertex();
|
||||
gid1 = vertex1.Gid();
|
||||
gid2 = vertex2.Gid();
|
||||
|
||||
ASSERT_NO_ERROR(vertex1.AddLabel(label2));
|
||||
ASSERT_NO_ERROR(vertex1.SetProperty(prop1, PropertyValue(1)));
|
||||
ASSERT_NO_ERROR(vertex2.AddLabel(label1));
|
||||
ASSERT_NO_ERROR(vertex2.SetProperty(prop1, PropertyValue(1)));
|
||||
|
||||
ASSERT_NO_ERROR(acc.Commit());
|
||||
}
|
||||
|
||||
{
|
||||
// tx1: B---AL(v1)-----OK-
|
||||
// tx2: -B---RL(v2)---OK--
|
||||
|
||||
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(vertex1->AddLabel(label1));
|
||||
ASSERT_NO_ERROR(vertex2->RemoveLabel(label1));
|
||||
|
||||
ASSERT_NO_ERROR(acc2.Commit());
|
||||
ASSERT_NO_ERROR(acc1.Commit());
|
||||
}
|
||||
|
||||
{
|
||||
// B---AL(v2)---FAIL
|
||||
|
||||
auto acc = storage.Access();
|
||||
auto vertex2 = acc.FindVertex(gid2, View::OLD);
|
||||
ASSERT_NO_ERROR(vertex2->AddLabel(label1));
|
||||
|
||||
auto res = acc.Commit();
|
||||
ASSERT_TRUE(res.HasError());
|
||||
EXPECT_EQ(res.GetError(),
|
||||
(ConstraintViolation{ConstraintViolation::Type::UNIQUE, label1,
|
||||
prop1}));
|
||||
}
|
||||
|
||||
{
|
||||
// B---RL(v1)---OK
|
||||
|
||||
auto acc = storage.Access();
|
||||
auto vertex1 = acc.FindVertex(gid1, View::OLD);
|
||||
ASSERT_NO_ERROR(vertex1->RemoveLabel(label1));
|
||||
ASSERT_NO_ERROR(acc.Commit());
|
||||
}
|
||||
|
||||
{
|
||||
// tx1: B---AL(v1)-----FAIL
|
||||
// tx2: -B---AL(v2)---OK---
|
||||
|
||||
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(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}));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user