Implement unique constraint functionality in query module

Reviewers: mferencevic

Reviewed By: mferencevic

Subscribers: pullbot

Differential Revision: https://phabricator.memgraph.io/D2681
This commit is contained in:
Tonko Sabolcec 2020-03-02 12:20:29 +01:00
parent 0e95934719
commit eed83a210e
7 changed files with 283 additions and 48 deletions

View File

@ -20,6 +20,7 @@
#ifdef MG_SINGLE_NODE_HA
#include "raft/exceptions.hpp"
#endif
#include "utils/algorithm.hpp"
#include "utils/exceptions.hpp"
#include "utils/flag_validation.hpp"
#include "utils/string.hpp"
@ -857,12 +858,22 @@ PreparedQuery PrepareInfoQuery(
auto *db = interpreter_context->db;
auto info = db->ListAllConstraints();
std::vector<std::vector<TypedValue>> results;
results.reserve(info.existence.size());
results.reserve(info.existence.size() + info.unique.size());
for (const auto &item : info.existence) {
results.push_back({TypedValue("exists"),
TypedValue(db->LabelToName(item.first)),
TypedValue(db->PropertyToName(item.second))});
}
for (const auto &item : info.unique) {
std::stringstream properties;
utils::PrintIterable(properties, item.second, ", ",
[&db](auto &stream, const auto &entry) {
stream << db->PropertyToName(entry);
});
results.push_back({TypedValue("unique"),
TypedValue(db->LabelToName(item.first)),
TypedValue(properties.str())});
}
return std::pair{results, QueryHandlerResult::NOTHING};
};
break;
@ -935,14 +946,61 @@ PreparedQuery PrepareConstraintQuery(
interpreter_context->db->PropertyToName(
*violation.properties.begin());
throw QueryRuntimeException(
"Unable to create a constraint :{}({}), because an existing "
"node violates it.",
"Unable to create existence constraint :{}({}), because an "
"existing node violates it.",
label_name, property_name);
}
};
break;
case Constraint::Type::UNIQUE:
throw utils::NotYetImplemented("Unique constraints");
std::set<storage::PropertyId> property_set;
for (const auto &property : properties) {
property_set.insert(property);
}
if (property_set.size() != properties.size()) {
throw SyntaxException(
"The given set of properties contains duplicates.");
}
handler = [interpreter_context, label,
property_set = std::move(property_set)] {
auto res = interpreter_context->db->CreateUniqueConstraint(
label, property_set);
if (res.HasError()) {
auto violation = res.GetError();
auto label_name =
interpreter_context->db->LabelToName(violation.label);
std::stringstream property_names_stream;
utils::PrintIterable(
property_names_stream, violation.properties, ", ",
[&interpreter_context](auto &stream, const auto &prop) {
stream << interpreter_context->db->PropertyToName(prop);
});
throw QueryRuntimeException(
"Unable to create unique constraint :{}({}), because an "
"existing node violates it.",
label_name, property_names_stream.str());
} else {
switch (res.GetValue()) {
case storage::UniqueConstraints::CreationStatus::
EMPTY_PROPERTIES:
throw SyntaxException(
"At least one property must be used for unique "
"constraints.");
break;
case storage::UniqueConstraints::CreationStatus::
PROPERTIES_SIZE_LIMIT_EXCEEDED:
throw SyntaxException(
"Too many properties specified. Limit of {} properties "
"for unique constraints is exceeded.",
storage::kUniqueConstraintsMaxProperties);
break;
case storage::UniqueConstraints::CreationStatus::ALREADY_EXISTS:
case storage::UniqueConstraints::CreationStatus::SUCCESS:
break;
}
}
};
break;
}
} break;
case ConstraintQuery::ActionType::DROP: {
@ -962,7 +1020,37 @@ PreparedQuery PrepareConstraintQuery(
};
break;
case Constraint::Type::UNIQUE:
throw utils::NotYetImplemented("Unique constraints");
std::set<storage::PropertyId> property_set;
for (const auto &property : properties) {
property_set.insert(property);
}
if (property_set.size() != properties.size()) {
throw SyntaxException(
"The given set of properties contains duplicates.");
}
handler = [interpreter_context, label,
property_set = std::move(property_set)] {
auto res = interpreter_context->db->DropUniqueConstraint(
label, property_set);
switch (res) {
case storage::UniqueConstraints::DeletionStatus::EMPTY_PROPERTIES:
throw SyntaxException(
"At least one property must be used for unique "
"constraints.");
break;
case storage::UniqueConstraints::DeletionStatus::
PROPERTIES_SIZE_LIMIT_EXCEEDED:
throw SyntaxException(
"Too many properties specified. Limit of {} properties for "
"unique constraints is exceeded.",
storage::kUniqueConstraintsMaxProperties);
break;
case storage::UniqueConstraints::DeletionStatus::NOT_FOUND:
case storage::UniqueConstraints::DeletionStatus::SUCCESS:
break;
}
return std::vector<std::vector<TypedValue>>();
};
}
} break;
}
@ -1111,17 +1199,37 @@ void Interpreter::Commit() {
auto maybe_constraint_violation = db_accessor_->Commit();
if (maybe_constraint_violation.HasError()) {
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(
switch (constraint_violation.type) {
case storage::ConstraintViolation::Type::EXISTENCE: {
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.properties.begin());
execution_db_accessor_ = std::nullopt;
db_accessor_ = std::nullopt;
throw QueryException(
"Unable to commit due to existence constraint violation on :{}({}).",
label_name, property_name);
execution_db_accessor_ = std::nullopt;
db_accessor_ = std::nullopt;
throw QueryException(
"Unable to commit due to existence constraint violation on :{}({})",
label_name, property_name);
break;
}
case storage::ConstraintViolation::Type::UNIQUE: {
auto label_name =
execution_db_accessor_->LabelToName(constraint_violation.label);
std::stringstream property_names_stream;
utils::PrintIterable(
property_names_stream, constraint_violation.properties, ", ",
[this](auto &stream, const auto &prop) {
stream << execution_db_accessor_->PropertyToName(prop);
});
execution_db_accessor_ = std::nullopt;
db_accessor_ = std::nullopt;
throw QueryException(
"Unable to commit due to unique constraint violation on :{}({})",
label_name, property_names_stream.str());
break;
}
}
}
execution_db_accessor_ = std::nullopt;
db_accessor_ = std::nullopt;

View File

@ -353,9 +353,11 @@ 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;
if (properties.empty()) {
return CreationStatus::EMPTY_PROPERTIES;
}
if (properties.size() > kUniqueConstraintsMaxProperties) {
return CreationStatus::PROPERTIES_SIZE_LIMIT_EXCEEDED;
}
auto [constraint, emplaced] = constraints_.emplace(
@ -404,6 +406,20 @@ UniqueConstraints::CreateConstraint(
return CreationStatus::SUCCESS;
}
UniqueConstraints::DeletionStatus UniqueConstraints::DropConstraint(
LabelId label, const std::set<PropertyId> &properties) {
if (properties.empty()) {
return UniqueConstraints::DeletionStatus::EMPTY_PROPERTIES;
}
if (properties.size() > kUniqueConstraintsMaxProperties) {
return UniqueConstraints::DeletionStatus::PROPERTIES_SIZE_LIMIT_EXCEEDED;
}
if (constraints_.erase({label, properties}) > 0) {
return UniqueConstraints::DeletionStatus::SUCCESS;
}
return UniqueConstraints::DeletionStatus::NOT_FOUND;
}
std::optional<ConstraintViolation> UniqueConstraints::Validate(
const Vertex &vertex, const Transaction &tx,
uint64_t commit_timestamp) const {

View File

@ -71,7 +71,16 @@ class UniqueConstraints {
enum class CreationStatus {
SUCCESS,
ALREADY_EXISTS,
INVALID_PROPERTIES_SIZE,
EMPTY_PROPERTIES,
PROPERTIES_SIZE_LIMIT_EXCEEDED,
};
/// Status for deletion of unique constraints.
enum class DeletionStatus {
SUCCESS,
NOT_FOUND,
EMPTY_PROPERTIES,
PROPERTIES_SIZE_LIMIT_EXCEEDED,
};
/// Indexes the given vertex for relevant labels and properties.
@ -83,18 +92,24 @@ class UniqueConstraints {
/// 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 `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.
/// constraint already existed, `CreationStatus::EMPTY_PROPERTIES` if the
/// given list of properties is empty,
/// `CreationStatus::PROPERTIES_SIZE_LIMIT_EXCEEDED` if the list of properties
/// exceeds the maximum allowed number of properties, and
/// `CreationStatus::SUCCESS` on success.
/// @throw std::bad_alloc
utils::BasicResult<ConstraintViolation, CreationStatus> CreateConstraint(
LabelId label, const std::set<PropertyId> &properties,
utils::SkipList<Vertex>::Accessor vertices);
bool DropConstraint(LabelId label, const std::set<PropertyId> &properties) {
return constraints_.erase({label, properties}) > 0;
}
/// Deletes the specified constraint. Returns `DeletionStatus::NOT_FOUND` if
/// there is not such constraint in the storage,
/// `DeletionStatus::EMPTY_PROPERTIES` if the given set of `properties` is
/// empty, `DeletionStatus::PROPERTIES_SIZE_LIMIT_EXCEEDED` if the given set
/// of `properties` exceeds the maximum allowed number of properties, and
/// `DeletionStatus::SUCCESS` on success.
DeletionStatus DropConstraint(LabelId label,
const std::set<PropertyId> &properties);
bool ConstraintExists(LabelId label, const std::set<PropertyId> &properties) {
return constraints_.find({label, properties}) != constraints_.end();

View File

@ -1066,8 +1066,8 @@ Storage::CreateUniqueConstraint(LabelId label,
vertices_.access());
}
bool Storage::DropUniqueConstraint(LabelId label,
const std::set<PropertyId> &properties) {
UniqueConstraints::DeletionStatus 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, properties);

View File

@ -355,18 +355,24 @@ class Storage final {
/// 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.
/// * `ALREADY_EXISTS` if the constraint already existed,
/// * `EMPTY_PROPERTIES` if the property set is empty, or
// * `PROPERTIES_SIZE_LIMIT_EXCEEDED` if the property set exceeds the
// limit of maximum number of properties.
///
/// @throw std::bad_alloc
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,
const std::set<PropertyId> &properties);
/// Removes a unique constraint. Returns `UniqueConstraints::DeletionStatus`
/// enum with the following possibilities:
/// * `SUCCESS` if constraint was successfully removed,
/// * `NOT_FOUND` if the specified constraint was not found,
/// * `EMPTY_PROPERTIES` if the property set is empty, or
/// * `PROPERTIES_SIZE_LIMIT_EXCEEDED` if the property set exceeds the
// limit of maximum number of properties.
UniqueConstraints::DeletionStatus DropUniqueConstraint(
LabelId label, const std::set<PropertyId> &properties);
ConstraintsInfo ListAllConstraints() const;

View File

@ -337,6 +337,78 @@ TEST_F(InterpreterTest, ExistenceConstraintTest) {
Interpret("CREATE (:A{a:2})");
Interpret("MATCH (n:A{a:2}) DETACH DELETE n");
Interpret("CREATE (n:A{a:2})");
ASSERT_THROW(Interpret("CREATE CONSTRAINT ON (n:A) ASSERT EXISTS (n.b);"),
query::QueryRuntimeException);
}
TEST_F(InterpreterTest, UniqueConstraintTest) {
// Empty property list should result with syntax exception.
ASSERT_THROW(Interpret("CREATE CONSTRAINT ON (n:A) ASSERT IS UNIQUE;"),
query::SyntaxException);
ASSERT_THROW(Interpret("DROP CONSTRAINT ON (n:A) ASSERT IS UNIQUE;"),
query::SyntaxException);
// Too large list of properties should also result with syntax exception.
{
std::stringstream stream;
stream << " ON (n:A) ASSERT ";
for (size_t i = 0; i < 33; ++i) {
if (i > 0) stream << ", ";
stream << "n.prop" << i;
}
stream << " IS UNIQUE;";
std::string create_query = "CREATE CONSTRAINT" + stream.str();
std::string drop_query = "DROP CONSTRAINT" + stream.str();
ASSERT_THROW(Interpret(create_query), query::SyntaxException);
ASSERT_THROW(Interpret(drop_query), query::SyntaxException);
}
// Providing property list with duplicates results with syntax exception.
ASSERT_THROW(
Interpret("CREATE CONSTRAINT ON (n:A) ASSERT n.a, n.b, n.a IS UNIQUE;"),
query::SyntaxException);
ASSERT_THROW(
Interpret("DROP CONSTRAINT ON (n:A) ASSERT n.a, n.b, n.a IS UNIQUE;"),
query::SyntaxException);
// Commit of vertex should fail if a constraint is violated.
Interpret("CREATE CONSTRAINT ON (n:A) ASSERT n.a, n.b IS UNIQUE;");
Interpret("CREATE (:A{a:1, b:2})");
Interpret("CREATE (:A{a:1, b:3})");
ASSERT_THROW(Interpret("CREATE (:A{a:1, b:2})"), query::QueryException);
// Attempt to create a constraint should fail if it's violated.
Interpret("CREATE (:A{a:1, c:2})");
Interpret("CREATE (:A{a:1, c:2})");
ASSERT_THROW(
Interpret("CREATE CONSTRAINT ON (n:A) ASSERT n.a, n.c IS UNIQUE;"),
query::QueryRuntimeException);
Interpret("MATCH (n:A{a:2, b:2}) SET n.a=1");
Interpret("CREATE (:A{a:2})");
Interpret("MATCH (n:A{a:2}) DETACH DELETE n");
Interpret("CREATE (n:A{a:2})");
// Show constraint info.
{
auto stream = Interpret("SHOW CONSTRAINT INFO");
ASSERT_EQ(stream.GetHeader().size(), 3U);
const auto &header = stream.GetHeader();
ASSERT_EQ(header[0], "constraint type");
ASSERT_EQ(header[1], "label");
ASSERT_EQ(header[2], "properties");
ASSERT_EQ(stream.GetResults().size(), 1U);
const auto &result = stream.GetResults().front();
ASSERT_EQ(result.size(), 3U);
ASSERT_EQ(result[0].ValueString(), "unique");
ASSERT_EQ(result[1].ValueString(), "A");
ASSERT_EQ(result[2].ValueString(), "a, b");
}
// Drop constraint.
Interpret("DROP CONSTRAINT ON (n:A) ASSERT n.a, n.b IS UNIQUE;");
// Removing the same constraint twice should not throw any exception.
Interpret("DROP CONSTRAINT ON (n:A) ASSERT n.a, n.b IS UNIQUE;");
}
TEST_F(InterpreterTest, ExplainQuery) {

View File

@ -182,7 +182,7 @@ TEST_F(ConstraintsTest, ExistenceConstraintsViolationOnCommit) {
}
// NOLINTNEXTLINE(hicpp-special-member-functions)
TEST_F(ConstraintsTest, UniqueConstraintsCreateAndDrop) {
TEST_F(ConstraintsTest, UniqueConstraintsCreateAndDropAndList) {
EXPECT_EQ(storage.ListAllConstraints().unique.size(), 0);
{
auto res = storage.CreateUniqueConstraint(label1, {prop1});
@ -211,13 +211,17 @@ TEST_F(ConstraintsTest, UniqueConstraintsCreateAndDrop) {
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_EQ(storage.DropUniqueConstraint(label1, {prop1}),
UniqueConstraints::DeletionStatus::SUCCESS);
EXPECT_EQ(storage.DropUniqueConstraint(label1, {prop1}),
UniqueConstraints::DeletionStatus::NOT_FOUND);
EXPECT_THAT(storage.ListAllConstraints().unique,
UnorderedElementsAre(
std::make_pair(label2, std::set<PropertyId>{prop1})));
EXPECT_TRUE(storage.DropUniqueConstraint(label2, {prop1}));
EXPECT_FALSE(storage.DropUniqueConstraint(label2, {prop2}));
EXPECT_EQ(storage.DropUniqueConstraint(label2, {prop1}),
UniqueConstraints::DeletionStatus::SUCCESS);
EXPECT_EQ(storage.DropUniqueConstraint(label2, {prop2}),
UniqueConstraints::DeletionStatus::NOT_FOUND);
EXPECT_EQ(storage.ListAllConstraints().unique.size(), 0);
{
auto res = storage.CreateUniqueConstraint(label2, {prop1});
@ -618,7 +622,6 @@ TEST_F(ConstraintsTest, UniqueConstraintsLabelAlteration) {
ASSERT_NO_ERROR(vertex1->RemoveLabel(label1));
ASSERT_NO_ERROR(vertex1->AddLabel(label1));
// Commit the first transaction.
ASSERT_NO_ERROR(acc1.Commit());
}
@ -682,9 +685,13 @@ TEST_F(ConstraintsTest, UniqueConstraintsPropertySetSize) {
auto res = storage.CreateUniqueConstraint(label1, {});
ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(),
UniqueConstraints::CreationStatus::INVALID_PROPERTIES_SIZE);
UniqueConstraints::CreationStatus::EMPTY_PROPERTIES);
}
// Removing a constraint with empty property set should also fail.
ASSERT_EQ(storage.DropUniqueConstraint(label1, {}),
UniqueConstraints::DeletionStatus::EMPTY_PROPERTIES);
// Create a set of 33 properties.
std::set<PropertyId> properties;
for (int i = 1; i <= 33; ++i) {
@ -696,10 +703,15 @@ TEST_F(ConstraintsTest, UniqueConstraintsPropertySetSize) {
// properties, which is 32.
auto res = storage.CreateUniqueConstraint(label1, properties);
ASSERT_TRUE(res.HasValue());
ASSERT_EQ(res.GetValue(),
UniqueConstraints::CreationStatus::INVALID_PROPERTIES_SIZE);
ASSERT_EQ(
res.GetValue(),
UniqueConstraints::CreationStatus::PROPERTIES_SIZE_LIMIT_EXCEEDED);
}
// An attempt to delete constraint with too large property set should fail.
ASSERT_EQ(storage.DropUniqueConstraint(label1, properties),
UniqueConstraints::DeletionStatus::PROPERTIES_SIZE_LIMIT_EXCEEDED);
// Remove one property from the set.
properties.erase(properties.begin());
@ -712,6 +724,11 @@ TEST_F(ConstraintsTest, UniqueConstraintsPropertySetSize) {
EXPECT_THAT(storage.ListAllConstraints().unique,
UnorderedElementsAre(std::make_pair(label1, properties)));
// Removing a constraint with 32 properties should succeed.
ASSERT_EQ(storage.DropUniqueConstraint(label1, properties),
UniqueConstraints::DeletionStatus::SUCCESS);
ASSERT_TRUE(storage.ListAllConstraints().unique.empty());
}
// NOLINTNEXTLINE(hicpp-special-member-functions)
@ -936,7 +953,8 @@ TEST_F(ConstraintsTest, UniqueConstraintsInsertDropInsert) {
ASSERT_NO_ERROR(acc.Commit());
}
ASSERT_TRUE(storage.DropUniqueConstraint(label1, {prop2, prop1}));
ASSERT_EQ(storage.DropUniqueConstraint(label1, {prop2, prop1}),
UniqueConstraints::DeletionStatus::SUCCESS);
{
auto acc = storage.Access();
@ -949,10 +967,10 @@ TEST_F(ConstraintsTest, UniqueConstraintsInsertDropInsert) {
}
TEST_F(ConstraintsTest, UniqueConstraintsComparePropertyValues) {
// Purpose of this test is to make sure that extracted property values
// are correctly compared.
// 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);