UniqueLabelPropertyConstraint implementation

Summary:
UniqueLabelPropertyConstraint defines label + property restriction on vertices.
Label + Property + PropertyValue for that property must be unique at any given moment.

Reviewers: msantl, ipaljak

Reviewed By: msantl

Subscribers: mferencevic, pullbot

Differential Revision: https://phabricator.memgraph.io/D1884
This commit is contained in:
Vinko Kasljevic 2019-03-11 14:56:05 +01:00
parent 11f3cb8838
commit 8a5842ef63
10 changed files with 660 additions and 9 deletions

View File

@ -52,6 +52,8 @@ set(mg_single_node_sources
storage/single_node/record_accessor.cpp
storage/single_node/vertex_accessor.cpp
storage/single_node/constraints/existence_constraints.cpp
storage/single_node/constraints/record.cpp
storage/single_node/constraints/unique_label_property_constraint.cpp
transactions/single_node/engine.cpp
memgraph_init.cpp
)

View File

@ -13,6 +13,7 @@
#include "database/single_node/graph_db.hpp"
#include "storage/common/types/types.hpp"
#include "storage/single_node/constraints/exceptions.hpp"
#include "storage/single_node/edge_accessor.hpp"
#include "storage/single_node/vertex_accessor.hpp"
#include "transactions/transaction.hpp"
@ -22,11 +23,6 @@
namespace database {
/** Thrown when inserting in an index with constraint. */
class IndexConstraintViolationException : public utils::BasicException {
using utils::BasicException::BasicException;
};
/** Thrown when creating an index which already exists. */
class IndexExistsException : public utils::BasicException {
using utils::BasicException::BasicException;

View File

@ -0,0 +1,12 @@
/// @file
#pragma once
#include "utils/exceptions.hpp"
namespace database {
/** Thrown when inserting in an unique constraint. */
class IndexConstraintViolationException : public utils::BasicException {
using utils::BasicException::BasicException;
};
} // namespace database

View File

@ -0,0 +1,62 @@
#include "storage/single_node/constraints/record.hpp"
#include "storage/single_node/constraints/exceptions.hpp"
#include "storage/single_node/mvcc/version_list.hpp"
#include "transactions/single_node/engine.hpp"
#include "transactions/transaction.hpp"
namespace storage::constraints::impl {
Record::Record(gid::Gid gid, const tx::Transaction &t)
: curr_gid(gid), tx_id_cre(t.id_) {}
void Record::Insert(gid::Gid gid, const tx::Transaction &t) {
// Insert
// - delete before or in this transaction and not aborted
// - insert before and aborted
// Throw SerializationError
// - delted of inserted after this transaction
// Throw IndexConstraintViolationException
// - insert before or in this transaction and not aborted
// - delete before and aborted
t.TakeLock(lock_);
if (t.id_ < tx_id_cre || (tx_id_exp != 0 && t.id_ < tx_id_exp))
throw mvcc::SerializationError();
bool has_entry = tx_id_exp == 0;
bool is_aborted = has_entry ? t.engine_.Info(tx_id_cre).is_aborted()
: t.engine_.Info(tx_id_exp).is_aborted();
if ((has_entry && !is_aborted) || (!has_entry && is_aborted))
throw database::IndexConstraintViolationException(
"Node couldn't be updated due to unique constraint violation!");
curr_gid = gid;
tx_id_cre = t.id_;
tx_id_exp = 0;
}
void Record::Remove(gid::Gid gid, const tx::Transaction &t) {
// Remove
// - insert before or in this transaction and not aborted
// - remove before and aborted
// Nothing
// - remove before or in this transaction and not aborted
// - insert before and aborted
// Throw SerializationError
// - delete or insert after this transaction
t.TakeLock(lock_);
DCHECK(gid == curr_gid);
if (t.id_ < tx_id_cre || (tx_id_exp != 0 && t.id_ < tx_id_exp))
throw mvcc::SerializationError();
bool has_entry = tx_id_exp == 0;
bool is_aborted = has_entry ? t.engine_.Info(tx_id_cre).is_aborted()
: t.engine_.Info(tx_id_exp).is_aborted();
if ((!has_entry && !is_aborted) || (has_entry && is_aborted)) return;
tx_id_exp = t.id_;
}
} // namespace storage::constraints::impl

View File

@ -0,0 +1,25 @@
/// @file
#pragma once
#include "storage/common/locking/record_lock.hpp"
#include "storage/single_node/gid.hpp"
#include "transactions/type.hpp"
namespace tx {
class Transaction;
} // namespace tx
namespace storage::constraints::impl {
/// Contains records of creation and deletion of entry in a constraint.
struct Record {
Record(gid::Gid gid, const tx::Transaction &t);
void Insert(gid::Gid gid, const tx::Transaction &t);
void Remove(gid::Gid gid, const tx::Transaction &t);
gid::Gid curr_gid;
tx::TransactionId tx_id_cre;
tx::TransactionId tx_id_exp{0};
RecordLock lock_;
};
} // namespace storage::constraints::impl

View File

@ -0,0 +1,143 @@
#include "storage/single_node/constraints/unique_label_property_constraint.hpp"
#include "storage/single_node/vertex_accessor.hpp"
#include "utils/algorithm.hpp"
namespace storage::constraints {
auto FindIn(storage::Label label, storage::Property property,
const std::list<impl::LabelPropertyEntry> &constraints) {
return std::find_if(constraints.begin(), constraints.end(),
[label, property](auto &c) {
return c.label == label && c.property == property;
});
}
void UniqueLabelPropertyConstraint::AddConstraint(storage::Label label,
storage::Property property,
const tx::Transaction &t) {
auto found = FindIn(label, property, constraints_);
if (found == constraints_.end()) constraints_.emplace_back(label, property);
}
void UniqueLabelPropertyConstraint::RemoveConstraint(
storage::Label label, storage::Property property) {
auto found = FindIn(label, property, constraints_);
if (found != constraints_.end()) constraints_.erase(found);
}
bool UniqueLabelPropertyConstraint::Exists(storage::Label label,
storage::Property property) const {
return FindIn(label, property, constraints_) != constraints_.end();
}
std::vector<LabelProperty> UniqueLabelPropertyConstraint::ListConstraints()
const {
std::vector<LabelProperty> constraints;
constraints.reserve(constraints_.size());
std::transform(constraints_.begin(), constraints_.end(),
std::back_inserter(constraints), [](auto &c) {
return LabelProperty{c.label, c.property};
});
return constraints;
}
void UniqueLabelPropertyConstraint::UpdateOnAddLabel(
storage::Label label, const VertexAccessor &accessor,
const tx::Transaction &t) {
auto &vertex = accessor.current();
std::lock_guard<std::mutex> guard(lock_);
for (auto &constraint : constraints_) {
auto value = vertex.properties_.at(constraint.property);
if (constraint.label == label && !value.IsNull()) {
bool found = false;
for (auto &p : constraint.version_pairs) {
if (p.value == value) {
p.record.Insert(accessor.gid(), t);
found = true;
break;
}
}
if (!found) {
constraint.version_pairs.emplace_back(accessor.gid(), value, t);
}
}
}
}
void UniqueLabelPropertyConstraint::UpdateOnRemoveLabel(
storage::Label label, const VertexAccessor &accessor,
const tx::Transaction &t) {
auto &vertex = accessor.current();
std::lock_guard<std::mutex> guard(lock_);
for (auto &constraint : constraints_) {
auto value = vertex.properties_.at(constraint.property);
if (constraint.label == label && !value.IsNull()) {
for (auto &p : constraint.version_pairs) {
if (p.value == value) {
p.record.Remove(accessor.gid(), t);
break;
}
}
}
}
}
void UniqueLabelPropertyConstraint::UpdateOnAddProperty(
storage::Property property, const PropertyValue &value,
const VertexAccessor &accessor, const tx::Transaction &t) {
auto &vertex = accessor.current();
std::lock_guard<std::mutex> guard(lock_);
for (auto &constraint : constraints_) {
if (constraint.property == property &&
utils::Contains(vertex.labels_, constraint.label)) {
bool found = false;
for (auto &p : constraint.version_pairs) {
if (p.value == value) {
p.record.Insert(accessor.gid(), t);
found = true;
break;
}
}
if (!found) {
constraint.version_pairs.emplace_back(accessor.gid(), value, t);
}
}
}
}
void UniqueLabelPropertyConstraint::UpdateOnRemoveProperty(
storage::Property property, const PropertyValue &value,
const VertexAccessor &accessor, const tx::Transaction &t) {
auto &vertex = accessor.current();
std::lock_guard<std::mutex> guard(lock_);
for (auto &constraint : constraints_) {
if (constraint.property == property &&
utils::Contains(vertex.labels_, constraint.label)) {
for (auto &p : constraint.version_pairs) {
if (p.value == value) {
p.record.Remove(accessor.gid(), t);
break;
}
}
}
}
}
void UniqueLabelPropertyConstraint::Refresh(const tx::Snapshot &snapshot,
const tx::Engine &engine) {
std::lock_guard<std::mutex> guard(lock_);
for (auto &constraint : constraints_) {
for (auto p = constraint.version_pairs.begin();
p != constraint.version_pairs.end(); ++p) {
auto exp_id = p->record.tx_id_exp;
auto cre_id = p->record.tx_id_cre;
if ((exp_id != 0 && exp_id < snapshot.back() &&
engine.Info(exp_id).is_committed() && !snapshot.contains(exp_id)) ||
(cre_id < snapshot.back() && engine.Info(cre_id).is_aborted())) {
constraint.version_pairs.erase(p);
}
}
}
}
} // namespace storage::constraints

View File

@ -0,0 +1,128 @@
/// @file
#pragma once
#include <list>
#include <mutex>
#include "storage/common/types/property_value.hpp"
#include "storage/common/types/types.hpp"
#include "storage/single_node/constraints/record.hpp"
namespace tx {
class Snapshot;
};
class VertexAccessor;
namespace storage::constraints {
namespace impl {
struct Pair {
Pair(gid::Gid gid, const PropertyValue &v, const tx::Transaction &t)
: value(v), record(gid, t) {}
PropertyValue value;
Record record;
};
/// Contains data for every unique constraint rule. It consists of label,
/// property and all property values for that property. For each property value
/// there is a record that contains ids of transactions that created and
/// deleted record with that value.
struct LabelPropertyEntry {
explicit LabelPropertyEntry(storage::Label l, storage::Property p)
: label(l), property(p) {}
storage::Label label;
storage::Property property;
std::list<Pair> version_pairs;
};
} // namespace impl
struct LabelProperty {
// This struct is used by ListConstraints method in order to avoid using
// std::pair or something like that.
storage::Label label;
storage::Property property;
};
/// UniqueLabelPropertyConstraint contains all unique constraints defined by
/// both label and property. To create or delete unique constraint, caller
/// must ensure that there are no other transactions running in parallel.
/// Additionally, for adding unique constraint caller must first call
/// AddConstraint to create unique constraint and then call UpdateOnAddLabel or
/// UpdateOnAddProperty for every existing Vertex. If there is a unique
/// constraint violation then caller must manually handle that by catching
/// exception and calling RemoveConstraint method. This is needed to ensure
/// logical correctness of transactions. Once created, client uses UpdateOn
/// methods to notify UniqueLabelPropertyConstraint about changes. In case of
/// violation UpdateOn methods throw IndexConstraintViolationException
/// exception. Methods can also throw SerializationError. This class is thread
/// safe.
class UniqueLabelPropertyConstraint {
public:
UniqueLabelPropertyConstraint() = default;
UniqueLabelPropertyConstraint(const UniqueLabelPropertyConstraint &) = delete;
UniqueLabelPropertyConstraint(UniqueLabelPropertyConstraint &&) = delete;
UniqueLabelPropertyConstraint &operator=(
const UniqueLabelPropertyConstraint &) = delete;
UniqueLabelPropertyConstraint &operator=(UniqueLabelPropertyConstraint &&) =
delete;
/// Add new unique constraint, if constraint already exists this method does
/// nothing. This method doesn't check if any of the existing vertices breaks
/// this constraint. Caller must do that instead. Caller must also ensure that
/// no other transaction is running in parallel.
void AddConstraint(storage::Label label, storage::Property property,
const tx::Transaction &t);
/// Removes existing unique constraint, if the constraint doesn't exist this
/// method does nothing. Caller must ensure that no other transaction is
/// running in parallel.
void RemoveConstraint(storage::Label label, storage::Property property);
/// Checks whether given unique constraint is visible.
bool Exists(storage::Label label, storage::Property property) const;
/// Returns list of unique constraints.
std::vector<LabelProperty> ListConstraints() const;
/// Updates unique constraint versions.
///
/// @throws IndexConstraintViolationException
/// @throws SerializationError
void UpdateOnAddLabel(storage::Label label, const VertexAccessor &accessor,
const tx::Transaction &t);
/// Updates unique constraint versions.
///
/// @throws SerializationError
void UpdateOnRemoveLabel(storage::Label label, const VertexAccessor &accessor,
const tx::Transaction &t);
/// Updates unique constraint versions.
///
/// @throws IndexConstraintViolationException
/// @throws SerializationError
void UpdateOnAddProperty(storage::Property property,
const PropertyValue &value,
const VertexAccessor &accessor,
const tx::Transaction &t);
/// Updates unique constraint versions.
///
/// @throws SerializationError
void UpdateOnRemoveProperty(storage::Property property,
const PropertyValue &value,
const VertexAccessor &accessor,
const tx::Transaction &t);
/// Removes records that are no longer visible.
void Refresh(const tx::Snapshot &snapshot, const tx::Engine &engine);
private:
std::mutex lock_;
std::list<impl::LabelPropertyEntry> constraints_;
};
} // namespace storage::constraints

View File

@ -149,6 +149,10 @@ class RecordAccessor {
*/
int64_t CypherId() const;
/** Returns the current version (either new_ or old_) set on this
* RecordAccessor. */
const TRecord &current() const;
protected:
/**
* Pointer to the version (either old_ or new_) that READ operations
@ -160,10 +164,6 @@ class RecordAccessor {
*/
mutable TRecord *current_{nullptr};
/** Returns the current version (either new_ or old_) set on this
* RecordAccessor. */
const TRecord &current() const;
private:
// The database accessor for which this record accessor is created
// Provides means of getting to the transaction and database functions.

View File

@ -285,6 +285,9 @@ target_link_libraries(${test_prefix}transaction_engine_single_node_ha mg-single-
add_unit_test(typed_value.cpp)
target_link_libraries(${test_prefix}typed_value mg-single-node kvstore_dummy_lib)
add_unit_test(unique_label_property_constraint.cpp)
target_link_libraries(${test_prefix}unique_label_property_constraint mg-single-node kvstore_dummy_lib)
# Test mg-communication
add_unit_test(bolt_chunked_decoder_buffer.cpp)

View File

@ -0,0 +1,280 @@
#include <gflags/gflags.h>
#include <glog/logging.h>
#include <gtest/gtest.h>
#include "database/single_node/graph_db.hpp"
#include "database/single_node/graph_db_accessor.hpp"
#include "storage/single_node/constraints/unique_label_property_constraint.hpp"
class UniqueLabelPropertyTest : public ::testing::Test {
public:
void SetUp() override {
auto dba = db_.AccessBlocking();
label_ = dba->Label("label");
property_ = dba->Property("property");
constraint_.AddConstraint(label_, property_, dba->transaction());
dba->Commit();
}
database::GraphDb db_;
storage::Label label_;
storage::Property property_;
PropertyValue value_{"value"};
storage::constraints::UniqueLabelPropertyConstraint constraint_;
};
TEST_F(UniqueLabelPropertyTest, BuildDrop) {
{
auto dba = db_.Access();
EXPECT_TRUE(constraint_.Exists(label_, property_));
dba->Commit();
}
{
auto dba = db_.Access();
constraint_.RemoveConstraint(label_, property_);
dba->Commit();
}
{
auto dba = db_.Access();
EXPECT_FALSE(constraint_.Exists(label_, property_));
dba->Commit();
}
}
TEST_F(UniqueLabelPropertyTest, BuildWithViolation) {
auto dba1 = db_.Access();
auto v1 = dba1->InsertVertex();
v1.add_label(label_);
v1.PropsSet(property_, value_);
auto v2 = dba1->InsertVertex();
v2.add_label(label_);
v2.PropsSet(property_, value_);
dba1->Commit();
auto dba2 = db_.Access();
auto v3 = dba2->FindVertex(v1.gid(), false);
auto v4 = dba2->FindVertex(v2.gid(), false);
constraint_.UpdateOnAddLabel(label_, v3, dba2->transaction());
EXPECT_THROW(constraint_.UpdateOnAddLabel(label_, v4, dba2->transaction()),
database::IndexConstraintViolationException);
EXPECT_THROW(constraint_.UpdateOnAddProperty(property_, value_, v4,
dba2->transaction()),
database::IndexConstraintViolationException);
}
TEST_F(UniqueLabelPropertyTest, InsertInsert) {
{
auto dba = db_.Access();
auto v = dba->InsertVertex();
v.add_label(label_);
constraint_.UpdateOnAddLabel(label_, v, dba->transaction());
v.PropsSet(property_, value_);
constraint_.UpdateOnAddProperty(property_, value_, v, dba->transaction());
dba->Commit();
}
{
auto dba = db_.Access();
auto v = dba->InsertVertex();
v.add_label(label_);
constraint_.UpdateOnAddLabel(label_, v, dba->transaction());
v.PropsSet(property_, value_);
EXPECT_THROW(constraint_.UpdateOnAddProperty(property_, value_, v,
dba->transaction()),
database::IndexConstraintViolationException);
EXPECT_THROW(constraint_.UpdateOnAddLabel(label_, v, dba->transaction()),
database::IndexConstraintViolationException);
dba->Commit();
}
}
TEST_F(UniqueLabelPropertyTest, InsertInsertDiffValues) {
{
auto dba = db_.Access();
auto v = dba->InsertVertex();
v.add_label(label_);
constraint_.UpdateOnAddLabel(label_, v, dba->transaction());
v.PropsSet(property_, value_);
constraint_.UpdateOnAddProperty(property_, value_, v, dba->transaction());
dba->Commit();
}
{
auto dba = db_.Access();
auto v = dba->InsertVertex();
PropertyValue other_value{"Some other value"};
v.add_label(label_);
constraint_.UpdateOnAddLabel(label_, v, dba->transaction());
v.PropsSet(property_, other_value);
constraint_.UpdateOnAddProperty(property_, other_value, v,
dba->transaction());
dba->Commit();
}
}
TEST_F(UniqueLabelPropertyTest, InsertAbortInsert) {
{
auto dba = db_.Access();
auto v = dba->InsertVertex();
v.add_label(label_);
constraint_.UpdateOnAddLabel(label_, v, dba->transaction());
v.PropsSet(property_, value_);
constraint_.UpdateOnAddProperty(property_, value_, v, dba->transaction());
dba->Abort();
}
{
auto dba = db_.Access();
auto v = dba->InsertVertex();
v.add_label(label_);
constraint_.UpdateOnAddLabel(label_, v, dba->transaction());
v.PropsSet(property_, value_);
constraint_.UpdateOnAddProperty(property_, value_, v, dba->transaction());
dba->Commit();
}
}
TEST_F(UniqueLabelPropertyTest, InsertRemoveAbortInsert) {
gid::Gid gid = 0;
{
auto dba = db_.Access();
auto v = dba->InsertVertex();
v.add_label(label_);
constraint_.UpdateOnAddLabel(label_, v, dba->transaction());
v.PropsSet(property_, value_);
constraint_.UpdateOnAddProperty(property_, value_, v, dba->transaction());
gid = v.gid();
dba->Commit();
}
{
auto dba = db_.Access();
auto v = dba->FindVertex(gid, false);
v.PropsErase(property_);
constraint_.UpdateOnRemoveProperty(property_, value_, v,
dba->transaction());
dba->Abort();
}
{
auto dba = db_.Access();
auto v = dba->InsertVertex();
v.add_label(label_);
constraint_.UpdateOnAddLabel(label_, v, dba->transaction());
v.PropsSet(property_, value_);
EXPECT_THROW(constraint_.UpdateOnAddProperty(property_, value_, v,
dba->transaction()),
database::IndexConstraintViolationException);
}
}
TEST_F(UniqueLabelPropertyTest, InsertInsertSameTransaction) {
{
auto dba = db_.Access();
auto v1 = dba->InsertVertex();
v1.add_label(label_);
constraint_.UpdateOnAddLabel(label_, v1, dba->transaction());
v1.PropsSet(property_, value_);
constraint_.UpdateOnAddProperty(property_, value_, v1, dba->transaction());
auto v2 = dba->InsertVertex();
v2.add_label(label_);
constraint_.UpdateOnAddLabel(label_, v2, dba->transaction());
v2.PropsSet(property_, value_);
EXPECT_THROW(constraint_.UpdateOnAddProperty(property_, value_, v2,
dba->transaction()),
database::IndexConstraintViolationException);
}
}
TEST_F(UniqueLabelPropertyTest, InsertInsertReversed) {
auto dba1 = db_.Access();
auto dba2 = db_.Access();
auto v2 = dba2->InsertVertex();
v2.add_label(label_);
constraint_.UpdateOnAddLabel(label_, v2, dba2->transaction());
v2.PropsSet(property_, value_);
constraint_.UpdateOnAddProperty(property_, value_, v2, dba2->transaction());
dba2->Commit();
auto v1 = dba1->InsertVertex();
v1.add_label(label_);
constraint_.UpdateOnAddLabel(label_, v1, dba1->transaction());
v1.PropsSet(property_, value_);
EXPECT_THROW(constraint_.UpdateOnAddProperty(property_, value_, v1,
dba1->transaction()),
mvcc::SerializationError);
}
TEST_F(UniqueLabelPropertyTest, InsertRemoveInsert) {
gid::Gid gid = 0;
{
auto dba = db_.Access();
auto v = dba->InsertVertex();
v.add_label(label_);
constraint_.UpdateOnAddLabel(label_, v, dba->transaction());
v.PropsSet(property_, value_);
constraint_.UpdateOnAddProperty(property_, value_, v, dba->transaction());
gid = v.gid();
dba->Commit();
}
{
auto dba = db_.Access();
auto v = dba->FindVertex(gid, false);
v.PropsErase(property_);
constraint_.UpdateOnRemoveProperty(property_, value_, v,
dba->transaction());
dba->Commit();
}
{
auto dba = db_.Access();
auto v = dba->InsertVertex();
v.add_label(label_);
constraint_.UpdateOnAddLabel(label_, v, dba->transaction());
v.PropsSet(property_, value_);
constraint_.UpdateOnAddProperty(property_, value_, v, dba->transaction());
}
}
TEST_F(UniqueLabelPropertyTest, InsertRemoveInsertSameTransaction) {
auto dba = db_.Access();
auto v = dba->InsertVertex();
v.add_label(label_);
constraint_.UpdateOnAddLabel(label_, v, dba->transaction());
v.PropsSet(property_, value_);
constraint_.UpdateOnAddProperty(property_, value_, v, dba->transaction());
v.PropsErase(property_);
constraint_.UpdateOnRemoveProperty(property_, value_, v, dba->transaction());
v.PropsSet(property_, value_);
constraint_.UpdateOnAddProperty(property_, value_, v, dba->transaction());
dba->Commit();
}
TEST_F(UniqueLabelPropertyTest, InsertDropInsert) {
{
auto dba = db_.Access();
auto v = dba->InsertVertex();
v.add_label(label_);
constraint_.UpdateOnAddLabel(label_, v, dba->transaction());
v.PropsSet(property_, value_);
constraint_.UpdateOnAddProperty(property_, value_, v, dba->transaction());
dba->Commit();
}
{
auto dba = db_.AccessBlocking();
constraint_.RemoveConstraint(label_, property_);
dba->Commit();
}
{
auto dba = db_.Access();
auto v = dba->InsertVertex();
v.add_label(label_);
constraint_.UpdateOnAddLabel(label_, v, dba->transaction());
v.PropsSet(property_, value_);
constraint_.UpdateOnAddProperty(property_, value_, v, dba->transaction());
dba->Commit();
}
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
google::InitGoogleLogging(argv[0]);
return RUN_ALL_TESTS();
}