Compare commits

...

15 Commits

Author SHA1 Message Date
Josip Mrden
2df6db50c0 Merge branch 'master' into use-constraints-as-indices-in-planning 2023-10-26 14:02:36 +02:00
Josip Mrden
bda353b23c Add unit test 2023-10-26 14:02:19 +02:00
Josip Mrden
d04fef8ce0 Merge branch 'master' into use-constraints-as-indices-in-planning 2023-10-25 17:15:37 +02:00
Josip Mrden
c83c924c22 Add e2e tests and unit tests 2023-10-25 15:22:04 +02:00
Josip Mrden
1ea78ca058 Clang tidy and helper methods for unit tests 2023-10-25 14:27:44 +02:00
Josip Mrden
d399b86479 Merge branch 'master' into use-constraints-as-indices-in-planning 2023-10-25 11:06:19 +02:00
Josip Mrden
0f27444ec4 Merge branch 'master' into use-constraints-as-indices-in-planning 2023-10-24 22:23:37 +02:00
Josip Mrden
10ace3f445 Merge branch 'master' into use-constraints-as-indices-in-planning 2023-10-24 22:23:20 +02:00
Josip Mrden
c56be187cc Merge branch 'master' into use-constraints-as-indices-in-planning 2023-10-23 13:30:56 +02:00
Josip Mrden
d908ce76cd Set first looking indices then constraints 2023-10-23 13:29:48 +02:00
Josip Mrden
ab40115b1e Remove some merge invalid actions 2023-10-23 12:24:41 +02:00
Josip Mrden
edfa183ff7 Add better method signatures 2023-10-23 12:10:16 +02:00
Josip Mrden
33310b5800 Merge branch 'master' into use-constraints-as-indices-in-planning 2023-10-23 11:26:18 +02:00
Josip Mrden
78833afd41 Merge branch 'master' into use-constraints-as-indices-in-planning 2023-10-19 14:38:13 +02:00
Josip Mrden
aed6a2de24 Add unique constraints capability of serving as indices 2023-10-11 19:14:08 +02:00
25 changed files with 816 additions and 18 deletions

View File

@@ -640,6 +640,14 @@ class DbAccessor final {
const std::set<storage::PropertyId> &properties) {
return accessor_->DropUniqueConstraint(label, properties);
}
bool UniqueConstraintExists(const storage::LabelId &label, const storage::PropertyId &prop) const {
return accessor_->UniqueConstraintExists(label, prop);
}
bool IndexedScanExists(const storage::LabelId &label, const storage::PropertyId &prop) const {
return LabelPropertyIndexExists(label, prop) || UniqueConstraintExists(label, prop);
}
};
class SubgraphDbAccessor final {

View File

@@ -45,7 +45,7 @@ class PostProcessor final {
template <class TPlanningContext>
std::unique_ptr<LogicalOperator> Rewrite(std::unique_ptr<LogicalOperator> plan, TPlanningContext *context) {
auto index_lookup_plan =
RewriteWithIndexLookup(std::move(plan), context->symbol_table, context->ast_storage, context->db);
RewriteWithIndexLookup(std::move(plan), context->symbol_table, context->ast_storage, context->db, parameters_);
return RewriteWithJoinRewriter(std::move(index_lookup_plan), context->symbol_table, context->ast_storage,
context->db);
}

View File

@@ -25,6 +25,7 @@
#include <gflags/gflags.h>
#include "query/parameters.hpp"
#include "query/plan/operator.hpp"
#include "query/plan/preprocess.hpp"
@@ -46,8 +47,8 @@ ExpressionRemovalResult RemoveExpressions(Expression *expr, const std::unordered
template <class TDbAccessor>
class IndexLookupRewriter final : public HierarchicalLogicalOperatorVisitor {
public:
IndexLookupRewriter(SymbolTable *symbol_table, AstStorage *ast_storage, TDbAccessor *db)
: symbol_table_(symbol_table), ast_storage_(ast_storage), db_(db) {}
IndexLookupRewriter(SymbolTable *symbol_table, AstStorage *ast_storage, TDbAccessor *db, const Parameters &parameters)
: symbol_table_(symbol_table), ast_storage_(ast_storage), db_(db), parameters_(parameters) {}
using HierarchicalLogicalOperatorVisitor::PostVisit;
using HierarchicalLogicalOperatorVisitor::PreVisit;
@@ -529,6 +530,7 @@ class IndexLookupRewriter final : public HierarchicalLogicalOperatorVisitor {
SymbolTable *symbol_table_;
AstStorage *ast_storage_;
TDbAccessor *db_;
const Parameters &parameters_;
// Collected filters, pending for examination if they can be used for advanced
// lookup operations (by index, node ID, ...).
Filters filters_;
@@ -575,7 +577,7 @@ class IndexLookupRewriter final : public HierarchicalLogicalOperatorVisitor {
}
void RewriteBranch(std::shared_ptr<LogicalOperator> *branch) {
IndexLookupRewriter<TDbAccessor> rewriter(symbol_table_, ast_storage_, db_);
IndexLookupRewriter<TDbAccessor> rewriter(symbol_table_, ast_storage_, db_, parameters_);
(*branch)->Accept(rewriter);
if (rewriter.new_root_) {
*branch = rewriter.new_root_;
@@ -651,7 +653,7 @@ class IndexLookupRewriter final : public HierarchicalLogicalOperatorVisitor {
continue;
}
const auto &property = filter.property_filter->property_;
if (!db_->LabelPropertyIndexExists(GetLabel(label), GetProperty(property))) {
if (!db_->IndexedScanExists(GetLabel(label), GetProperty(property))) {
continue;
}
auto is_better_type = [&found](PropertyFilter::Type type) {
@@ -663,7 +665,10 @@ class IndexLookupRewriter final : public HierarchicalLogicalOperatorVisitor {
return type_sort_ix < found_sort_ix;
};
int64_t vertex_count = db_->VerticesCount(GetLabel(label), GetProperty(property));
std::optional<storage::PropertyValue> maybe_property_value = ConstPropertyValue(filter.property_filter->value_);
int64_t vertex_count = maybe_property_value.has_value()
? db_->VerticesCount(GetLabel(label), GetProperty(property), *maybe_property_value)
: db_->VerticesCount(GetLabel(label), GetProperty(property));
std::optional<storage::LabelPropertyIndexStats> new_stats =
db_->GetIndexStats(GetLabel(label), GetProperty(property));
@@ -791,6 +796,18 @@ class IndexLookupRewriter final : public HierarchicalLogicalOperatorVisitor {
filter_exprs_for_removal_.insert(removed_expressions.begin(), removed_expressions.end());
return std::make_unique<ScanAllByLabel>(input, node_symbol, GetLabel(label), view);
}
// If the expression is a constant property value, it is returned. Otherwise,
// return nullopt.
std::optional<storage::PropertyValue> ConstPropertyValue(const Expression *expression) {
if (const auto *literal = utils::Downcast<const PrimitiveLiteral>(expression); literal) {
return literal->value_;
}
if (const auto *param_lookup = utils::Downcast<const ParameterLookup>(expression); param_lookup) {
return parameters_.AtTokenPosition(param_lookup->token_position_);
}
return std::nullopt;
}
};
} // namespace impl
@@ -798,8 +815,8 @@ class IndexLookupRewriter final : public HierarchicalLogicalOperatorVisitor {
template <class TDbAccessor>
std::unique_ptr<LogicalOperator> RewriteWithIndexLookup(std::unique_ptr<LogicalOperator> root_op,
SymbolTable *symbol_table, AstStorage *ast_storage,
TDbAccessor *db) {
impl::IndexLookupRewriter<TDbAccessor> rewriter(symbol_table, ast_storage, db);
TDbAccessor *db, const Parameters &parameters) {
impl::IndexLookupRewriter<TDbAccessor> rewriter(symbol_table, ast_storage, db, parameters);
root_op->Accept(rewriter);
if (rewriter.new_root_) {
// This shouldn't happen in real use case, because IndexLookupRewriter

View File

@@ -87,6 +87,14 @@ class VertexCountCache {
return db_->GetIndexStats(label, property);
}
bool UniqueConstraintExists(const storage::LabelId &label, const storage::PropertyId &prop) const {
return db_->UniqueConstraintExists(label, prop);
}
bool IndexedScanExists(const storage::LabelId &label, const storage::PropertyId &prop) const {
return db_->IndexedScanExists(label, prop);
}
private:
typedef std::pair<storage::LabelId, storage::PropertyId> LabelPropertyKey;

View File

@@ -16,6 +16,7 @@
#include "storage/v2/constraints/constraint_violation.hpp"
#include "storage/v2/transaction.hpp"
#include "storage/v2/vertex.hpp"
#include "storage/v2/vertex_accessor.hpp"
#include "utils/result.hpp"
namespace memgraph::storage {
@@ -58,6 +59,13 @@ class UniqueConstraints {
virtual std::vector<std::pair<LabelId, std::set<PropertyId>>> ListConstraints() const = 0;
virtual uint64_t ApproximateVertexCount(const LabelId &label, const PropertyId &property) const = 0;
virtual uint64_t ApproximateVertexCount(const LabelId &label, const PropertyId &property,
const PropertyValue &value) const = 0;
virtual uint64_t ApproximateVertexCount(const LabelId &label, const PropertyId &property,
const std::optional<utils::Bound<PropertyValue>> &,
const std::optional<utils::Bound<PropertyValue>> &) const = 0;
virtual void Clear() = 0;
protected:

View File

@@ -1957,6 +1957,11 @@ UniqueConstraints::DeletionStatus DiskStorage::DiskAccessor::DropUniqueConstrain
return UniqueConstraints::DeletionStatus::SUCCESS;
}
bool DiskStorage::DiskAccessor::UniqueConstraintExists(const LabelId & /*label*/,
const PropertyId & /*property*/) const {
return false;
}
Transaction DiskStorage::CreateTransaction(IsolationLevel isolation_level, StorageMode storage_mode) {
/// We acquire the transaction engine lock here because we access (and
/// modify) the transaction engine variables (`transaction_id` and

View File

@@ -170,6 +170,8 @@ class DiskStorage final : public Storage {
UniqueConstraints::DeletionStatus DropUniqueConstraint(LabelId label,
const std::set<PropertyId> &properties) override;
bool UniqueConstraintExists(const LabelId &label, const PropertyId &property) const override;
};
std::unique_ptr<Storage::Accessor> Access(std::optional<IsolationLevel> override_isolation_level) override;

View File

@@ -310,6 +310,21 @@ std::vector<std::pair<LabelId, std::set<PropertyId>>> DiskUniqueConstraints::Lis
return {constraints_.begin(), constraints_.end()};
}
uint64_t DiskUniqueConstraints::ApproximateVertexCount(const LabelId & /*label*/,
const PropertyId & /*property*/) const {
return 10;
};
uint64_t DiskUniqueConstraints::ApproximateVertexCount(const LabelId & /*label*/, const PropertyId & /*property*/,
const PropertyValue & /*value*/) const {
return 10;
};
uint64_t DiskUniqueConstraints::ApproximateVertexCount(
const LabelId & /*label*/, const PropertyId & /*property*/,
const std::optional<utils::Bound<PropertyValue>> & /*lower*/,
const std::optional<utils::Bound<PropertyValue>> & /*upper*/) const {
return 10;
};
void DiskUniqueConstraints::Clear() {
constraints_.clear();

View File

@@ -54,6 +54,12 @@ class DiskUniqueConstraints : public UniqueConstraints {
std::vector<std::pair<LabelId, std::set<PropertyId>>> ListConstraints() const override;
uint64_t ApproximateVertexCount(const LabelId &label, const PropertyId &property) const override;
uint64_t ApproximateVertexCount(const LabelId &label, const PropertyId &property,
const PropertyValue &value) const override;
uint64_t ApproximateVertexCount(const LabelId &label, const PropertyId &property,
const std::optional<utils::Bound<PropertyValue>> &lower,
const std::optional<utils::Bound<PropertyValue>> &upper) const override;
void Clear() override;
RocksDBStorage *GetRocksDBStorage() const;

View File

@@ -1165,12 +1165,28 @@ UniqueConstraints::DeletionStatus InMemoryStorage::InMemoryAccessor::DropUniqueC
return UniqueConstraints::DeletionStatus::SUCCESS;
}
bool InMemoryStorage::InMemoryAccessor::UniqueConstraintExists(const LabelId &label, const PropertyId &property) const {
auto *in_memory = static_cast<InMemoryStorage *>(storage_);
auto *mem_unique_constraints =
static_cast<InMemoryUniqueConstraints *>(in_memory->constraints_.unique_constraints_.get());
return mem_unique_constraints->ConstraintExists(label, {property});
}
VerticesIterable InMemoryStorage::InMemoryAccessor::Vertices(LabelId label, View view) {
auto *mem_label_index = static_cast<InMemoryLabelIndex *>(storage_->indices_.label_index_.get());
return VerticesIterable(mem_label_index->Vertices(label, view, storage_, &transaction_));
}
VerticesIterable InMemoryStorage::InMemoryAccessor::Vertices(LabelId label, PropertyId property, View view) {
auto *mem_storage = static_cast<InMemoryStorage *>(storage_);
if (mem_storage->constraints_.unique_constraints_->ConstraintExists(label, {property})) {
auto *mem_unique_constraints =
static_cast<InMemoryUniqueConstraints *>(mem_storage->constraints_.unique_constraints_.get());
return VerticesIterable(
mem_unique_constraints->Vertices(label, property, std::nullopt, std::nullopt, view, storage_, &transaction_));
}
auto *mem_label_property_index =
static_cast<InMemoryLabelPropertyIndex *>(storage_->indices_.label_property_index_.get());
return VerticesIterable(
@@ -1179,6 +1195,15 @@ VerticesIterable InMemoryStorage::InMemoryAccessor::Vertices(LabelId label, Prop
VerticesIterable InMemoryStorage::InMemoryAccessor::Vertices(LabelId label, PropertyId property,
const PropertyValue &value, View view) {
auto *mem_storage = static_cast<InMemoryStorage *>(storage_);
if (mem_storage->constraints_.unique_constraints_->ConstraintExists(label, {property})) {
auto *mem_unique_constraints =
static_cast<InMemoryUniqueConstraints *>(mem_storage->constraints_.unique_constraints_.get());
return VerticesIterable(mem_unique_constraints->Vertices(label, property, utils::MakeBoundInclusive(value),
utils::MakeBoundInclusive(value), view, storage_,
&transaction_));
}
auto *mem_label_property_index =
static_cast<InMemoryLabelPropertyIndex *>(storage_->indices_.label_property_index_.get());
return VerticesIterable(mem_label_property_index->Vertices(label, property, utils::MakeBoundInclusive(value),
@@ -1189,6 +1214,14 @@ VerticesIterable InMemoryStorage::InMemoryAccessor::Vertices(LabelId label, Prop
VerticesIterable InMemoryStorage::InMemoryAccessor::Vertices(
LabelId label, PropertyId property, const std::optional<utils::Bound<PropertyValue>> &lower_bound,
const std::optional<utils::Bound<PropertyValue>> &upper_bound, View view) {
auto *mem_storage = static_cast<InMemoryStorage *>(storage_);
if (mem_storage->constraints_.unique_constraints_->ConstraintExists(label, {property})) {
auto *mem_unique_constraints =
static_cast<InMemoryUniqueConstraints *>(mem_storage->constraints_.unique_constraints_.get());
return VerticesIterable(
mem_unique_constraints->Vertices(label, property, lower_bound, upper_bound, view, storage_, &transaction_));
}
auto *mem_label_property_index =
static_cast<InMemoryLabelPropertyIndex *>(storage_->indices_.label_property_index_.get());
return VerticesIterable(

View File

@@ -115,16 +115,24 @@ class InMemoryStorage final : public Storage {
/// Return approximate number of vertices with the given label and property.
/// Note that this is always an over-estimate and never an under-estimate.
uint64_t ApproximateVertexCount(LabelId label, PropertyId property) const override {
return static_cast<InMemoryStorage *>(storage_)->indices_.label_property_index_->ApproximateVertexCount(label,
property);
auto *mem_storage = static_cast<InMemoryStorage *>(storage_);
if (mem_storage->indices_.label_property_index_->IndexExists(label, property)) {
return mem_storage->indices_.label_property_index_->ApproximateVertexCount(label, property);
}
return mem_storage->constraints_.unique_constraints_->ApproximateVertexCount(label, property);
}
/// Return approximate number of vertices with the given label and the given
/// value for the given property. Note that this is always an over-estimate
/// and never an under-estimate.
uint64_t ApproximateVertexCount(LabelId label, PropertyId property, const PropertyValue &value) const override {
return static_cast<InMemoryStorage *>(storage_)->indices_.label_property_index_->ApproximateVertexCount(
label, property, value);
auto *mem_storage = static_cast<InMemoryStorage *>(storage_);
if (mem_storage->indices_.label_property_index_->IndexExists(label, property)) {
return mem_storage->indices_.label_property_index_->ApproximateVertexCount(label, property, value);
}
return mem_storage->constraints_.unique_constraints_->ApproximateVertexCount(label, property, value);
}
/// Return approximate number of vertices with the given label and value for
@@ -133,8 +141,11 @@ class InMemoryStorage final : public Storage {
uint64_t ApproximateVertexCount(LabelId label, PropertyId property,
const std::optional<utils::Bound<PropertyValue>> &lower,
const std::optional<utils::Bound<PropertyValue>> &upper) const override {
return static_cast<InMemoryStorage *>(storage_)->indices_.label_property_index_->ApproximateVertexCount(
label, property, lower, upper);
auto *mem_storage = static_cast<InMemoryStorage *>(storage_);
if (mem_storage->indices_.label_property_index_->IndexExists(label, property)) {
return mem_storage->indices_.label_property_index_->ApproximateVertexCount(label, property, lower, upper);
}
return mem_storage->constraints_.unique_constraints_->ApproximateVertexCount(label, property, lower, upper);
}
template <typename TResult, typename TIndex, typename TIndexKey>
@@ -193,6 +204,8 @@ class InMemoryStorage final : public Storage {
return static_cast<InMemoryStorage *>(storage_)->indices_.label_property_index_->IndexExists(label, property);
}
bool UniqueConstraintExists(const LabelId &label, const PropertyId &property) const override;
IndicesInfo ListAllIndices() const override;
ConstraintsInfo ListAllConstraints() const override;

View File

@@ -10,6 +10,10 @@
// licenses/APL.txt.
#include "storage/v2/inmemory/unique_constraints.hpp"
#include "storage/v2/indices/indices_utils.hpp"
#include "storage/v2/property_value.hpp"
#include "utils/bound.hpp"
#include "utils/logging.hpp"
namespace memgraph::storage {
@@ -254,6 +258,16 @@ bool InMemoryUniqueConstraints::Entry::operator<(const std::vector<PropertyValue
bool InMemoryUniqueConstraints::Entry::operator==(const std::vector<PropertyValue> &rhs) const { return values == rhs; }
bool InMemoryUniqueConstraints::Entry::operator<(const PropertyValue &rhs) const {
MG_ASSERT(values.size() == 1, "Using unique constraint with multiple properties to compare with single property!");
return values[0] < rhs;
}
bool InMemoryUniqueConstraints::Entry::operator==(const PropertyValue &rhs) const {
MG_ASSERT(values.size() == 1, "Using unique constraint with multiple properties to compare with single property!");
return values[0] == rhs;
}
void InMemoryUniqueConstraints::UpdateBeforeCommit(const Vertex *vertex, const Transaction &tx) {
for (const auto &label : vertex->labels) {
if (!constraints_by_label_.contains(label)) {
@@ -407,6 +421,32 @@ std::vector<std::pair<LabelId, std::set<PropertyId>>> InMemoryUniqueConstraints:
return ret;
}
uint64_t InMemoryUniqueConstraints::ApproximateVertexCount(const LabelId &label, const PropertyId &property) const {
auto it = constraints_.find({label, {property}});
MG_ASSERT(it != constraints_.end(), "Unique constraints for label {} and property {} doesn't exist", label.AsUint(),
property.AsUint());
return 1;
};
uint64_t InMemoryUniqueConstraints::ApproximateVertexCount(const LabelId &label, const PropertyId &property,
const PropertyValue & /*value*/) const {
auto it = constraints_.find({label, {property}});
MG_ASSERT(it != constraints_.end(), "Unique constraints for label {} and property {} doesn't exist", label.AsUint(),
property.AsUint());
return 1;
};
uint64_t InMemoryUniqueConstraints::ApproximateVertexCount(
const LabelId &label, const PropertyId &property, const std::optional<utils::Bound<PropertyValue>> &lower,
const std::optional<utils::Bound<PropertyValue>> &upper) const {
auto it = constraints_.find({label, {property}});
MG_ASSERT(it != constraints_.end(), "Unique constraints for label {} and property {} doesn't exist", label.AsUint(),
property.AsUint());
auto acc = it->second.access();
// NOLINTNEXTLINE(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions)
return acc.estimate_range_count(lower, upper, utils::SkipListLayerForCountEstimation(acc.size()));
};
void InMemoryUniqueConstraints::RemoveObsoleteEntries(uint64_t oldest_active_start_timestamp) {
for (auto &[label_props, storage] : constraints_) {
auto acc = storage.access();
@@ -434,4 +474,201 @@ void InMemoryUniqueConstraints::Clear() {
constraints_by_label_.clear();
}
InMemoryUniqueConstraints::Iterable::Iterator::Iterator(Iterable *self,
utils::SkipList<Entry>::Iterator constraint_iterator)
: self_(self),
constraint_iterator_(constraint_iterator),
current_vertex_accessor_(nullptr, self_->storage_, nullptr),
current_vertex_(nullptr) {
AdvanceUntilValid();
}
InMemoryUniqueConstraints::Iterable::Iterator &InMemoryUniqueConstraints::Iterable::Iterator::operator++() {
++constraint_iterator_;
AdvanceUntilValid();
return *this;
}
void InMemoryUniqueConstraints::Iterable::Iterator::AdvanceUntilValid() {
for (; constraint_iterator_ != self_->constraint_accessor_.end(); ++constraint_iterator_) {
if (constraint_iterator_->vertex == current_vertex_) {
continue;
}
auto iterator_value = constraint_iterator_->values[0];
if (self_->lower_bound_) {
if (iterator_value < self_->lower_bound_->value()) {
continue;
}
if (!self_->lower_bound_->IsInclusive() && iterator_value == self_->lower_bound_->value()) {
continue;
}
}
if (self_->upper_bound_) {
if (self_->upper_bound_->value() < iterator_value) {
constraint_iterator_ = self_->constraint_accessor_.end();
break;
}
if (!self_->upper_bound_->IsInclusive() && iterator_value == self_->upper_bound_->value()) {
constraint_iterator_ = self_->constraint_accessor_.end();
break;
}
}
if (CurrentVersionHasLabelProperty(*constraint_iterator_->vertex, self_->label_, self_->property_, iterator_value,
self_->transaction_, self_->view_)) {
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
current_vertex_ = const_cast<Vertex *>(constraint_iterator_->vertex);
current_vertex_accessor_ = VertexAccessor(current_vertex_, self_->storage_, self_->transaction_);
break;
}
}
}
// These constants represent the smallest possible value of each type that is
// contained in a `PropertyValue`. Note that numbers (integers and doubles) are
// treated as the same "type" in `PropertyValue`.
const PropertyValue kSmallestBool = PropertyValue(false);
// NOLINTNEXTLINE(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions)
static_assert(-std::numeric_limits<double>::infinity() < std::numeric_limits<int64_t>::min());
const PropertyValue kSmallestNumber = PropertyValue(-std::numeric_limits<double>::infinity());
const PropertyValue kSmallestString = PropertyValue("");
const PropertyValue kSmallestList = PropertyValue(std::vector<PropertyValue>());
const PropertyValue kSmallestMap = PropertyValue(std::map<std::string, PropertyValue>());
const PropertyValue kSmallestTemporalData =
PropertyValue(TemporalData{static_cast<TemporalType>(0), std::numeric_limits<int64_t>::min()});
InMemoryUniqueConstraints::Iterable::Iterable(utils::SkipList<Entry>::Accessor constraint_accessor, LabelId label,
PropertyId property,
const std::optional<utils::Bound<PropertyValue>> &lower_bound,
const std::optional<utils::Bound<PropertyValue>> &upper_bound, View view,
Storage *storage, Transaction *transaction)
: constraint_accessor_(std::move(constraint_accessor)),
label_(label),
property_(property),
lower_bound_(lower_bound),
upper_bound_(upper_bound),
view_(view),
storage_(storage),
transaction_(transaction) {
// 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
// are returned by the iterator. We ensure this by supplying either an
// inclusive lower bound of the same type, or an exclusive upper bound of the
// following type. If neither bound is set we yield all items in the index.
// First we statically verify that our assumptions about the `PropertyValue`
// type ordering holds.
static_assert(PropertyValue::Type::Bool < PropertyValue::Type::Int);
static_assert(PropertyValue::Type::Int < PropertyValue::Type::Double);
static_assert(PropertyValue::Type::Double < PropertyValue::Type::String);
static_assert(PropertyValue::Type::String < PropertyValue::Type::List);
static_assert(PropertyValue::Type::List < PropertyValue::Type::Map);
// Remove any bounds that are set to `Null` because that isn't a valid value.
if (lower_bound_ && lower_bound_->value().IsNull()) {
lower_bound_ = std::nullopt;
}
if (upper_bound_ && upper_bound_->value().IsNull()) {
upper_bound_ = std::nullopt;
}
// Check whether the bounds are of comparable types if both are supplied.
if (lower_bound_ && upper_bound_ &&
!PropertyValue::AreComparableTypes(lower_bound_->value().type(), upper_bound_->value().type())) {
bounds_valid_ = false;
return;
}
// Set missing bounds.
if (lower_bound_ && !upper_bound_) {
// Here we need to supply an upper bound. The upper bound is set to an
// exclusive lower bound of the following type.
switch (lower_bound_->value().type()) {
case PropertyValue::Type::Null:
// This shouldn't happen because of the nullopt-ing above.
LOG_FATAL("Invalid database state!");
break;
case PropertyValue::Type::Bool:
upper_bound_ = utils::MakeBoundExclusive(kSmallestNumber);
break;
case PropertyValue::Type::Int:
case PropertyValue::Type::Double:
// Both integers and doubles are treated as the same type in
// `PropertyValue` and they are interleaved when sorted.
upper_bound_ = utils::MakeBoundExclusive(kSmallestString);
break;
case PropertyValue::Type::String:
upper_bound_ = utils::MakeBoundExclusive(kSmallestList);
break;
case PropertyValue::Type::List:
upper_bound_ = utils::MakeBoundExclusive(kSmallestMap);
break;
case PropertyValue::Type::Map:
upper_bound_ = utils::MakeBoundExclusive(kSmallestTemporalData);
break;
case PropertyValue::Type::TemporalData:
// This is the last type in the order so we leave the upper bound empty.
break;
}
}
if (upper_bound_ && !lower_bound_) {
// Here we need to supply a lower bound. The lower bound is set to an
// inclusive lower bound of the current type.
switch (upper_bound_->value().type()) {
case PropertyValue::Type::Null:
// This shouldn't happen because of the nullopt-ing above.
LOG_FATAL("Invalid database state!");
break;
case PropertyValue::Type::Bool:
lower_bound_ = utils::MakeBoundInclusive(kSmallestBool);
break;
case PropertyValue::Type::Int:
case PropertyValue::Type::Double:
// Both integers and doubles are treated as the same type in
// `PropertyValue` and they are interleaved when sorted.
lower_bound_ = utils::MakeBoundInclusive(kSmallestNumber);
break;
case PropertyValue::Type::String:
lower_bound_ = utils::MakeBoundInclusive(kSmallestString);
break;
case PropertyValue::Type::List:
lower_bound_ = utils::MakeBoundInclusive(kSmallestList);
break;
case PropertyValue::Type::Map:
lower_bound_ = utils::MakeBoundInclusive(kSmallestMap);
break;
case PropertyValue::Type::TemporalData:
lower_bound_ = utils::MakeBoundInclusive(kSmallestTemporalData);
break;
}
}
}
InMemoryUniqueConstraints::Iterable::Iterator InMemoryUniqueConstraints::Iterable::begin() {
// If the bounds are set and don't have comparable types we don't yield any
// items from the index.
if (!bounds_valid_) return {this, constraint_accessor_.end()};
auto constraint_iterator = constraint_accessor_.begin();
if (lower_bound_) {
constraint_iterator = constraint_accessor_.find_equal_or_greater(std::vector<PropertyValue>{lower_bound_->value()});
}
return {this, constraint_iterator};
}
InMemoryUniqueConstraints::Iterable::Iterator InMemoryUniqueConstraints::Iterable::end() {
return {this, constraint_accessor_.end()};
}
InMemoryUniqueConstraints::Iterable InMemoryUniqueConstraints::Vertices(
LabelId label, PropertyId property, const std::optional<utils::Bound<PropertyValue>> &lower_bound,
const std::optional<utils::Bound<PropertyValue>> &upper_bound, View view, Storage *storage,
Transaction *transaction) {
auto it = constraints_.find({label, {property}});
MG_ASSERT(it != constraints_.end(), "Constraint for label {} and property {} doesn't exist", label.AsUint(),
property.AsUint());
return {it->second.access(), label, property, lower_bound, upper_bound, view, storage, transaction};
}
} // namespace memgraph::storage

View File

@@ -42,6 +42,9 @@ class InMemoryUniqueConstraints : public UniqueConstraints {
bool operator<(const std::vector<PropertyValue> &rhs) const;
bool operator==(const std::vector<PropertyValue> &rhs) const;
bool operator<(const PropertyValue &rhs) const;
bool operator==(const PropertyValue &rhs) const;
};
public:
@@ -92,11 +95,64 @@ class InMemoryUniqueConstraints : public UniqueConstraints {
std::vector<std::pair<LabelId, std::set<PropertyId>>> ListConstraints() const override;
class Iterable {
public:
Iterable(utils::SkipList<Entry>::Accessor constraint_accessor, LabelId label, PropertyId property,
const std::optional<utils::Bound<PropertyValue>> &lower_bound,
const std::optional<utils::Bound<PropertyValue>> &upper_bound, View view, Storage *storage,
Transaction *transaction);
class Iterator {
public:
Iterator(Iterable *self, utils::SkipList<Entry>::Iterator constraint_iterator);
VertexAccessor const &operator*() const { return current_vertex_accessor_; }
bool operator==(const Iterator &other) const { return constraint_iterator_ == other.constraint_iterator_; }
bool operator!=(const Iterator &other) const { return constraint_iterator_ != other.constraint_iterator_; }
Iterator &operator++();
private:
void AdvanceUntilValid();
Iterable *self_;
utils::SkipList<Entry>::Iterator constraint_iterator_;
VertexAccessor current_vertex_accessor_;
Vertex *current_vertex_;
};
Iterator begin();
Iterator end();
private:
utils::SkipList<Entry>::Accessor constraint_accessor_;
LabelId label_;
PropertyId property_;
std::optional<utils::Bound<PropertyValue>> lower_bound_;
std::optional<utils::Bound<PropertyValue>> upper_bound_;
bool bounds_valid_{true};
View view_;
Storage *storage_;
Transaction *transaction_;
};
uint64_t ApproximateVertexCount(const LabelId &label, const PropertyId &property) const override;
uint64_t ApproximateVertexCount(const LabelId &label, const PropertyId &property,
const PropertyValue &value) const override;
uint64_t ApproximateVertexCount(const LabelId &label, const PropertyId &property,
const std::optional<utils::Bound<PropertyValue>> &lower,
const std::optional<utils::Bound<PropertyValue>> &upper) const override;
/// GC method that removes outdated entries from constraints' storages.
void RemoveObsoleteEntries(uint64_t oldest_active_start_timestamp);
void Clear() override;
Iterable Vertices(LabelId label, PropertyId property, const std::optional<utils::Bound<PropertyValue>> &lower_bound,
const std::optional<utils::Bound<PropertyValue>> &upper_bound, View view, Storage *storage,
Transaction *transaction);
private:
std::map<std::pair<LabelId, std::set<PropertyId>>, utils::SkipList<Entry>> constraints_;
std::map<LabelId, std::map<std::set<PropertyId>, utils::SkipList<Entry> *>> constraints_by_label_;

View File

@@ -257,6 +257,8 @@ class Storage {
virtual UniqueConstraints::DeletionStatus DropUniqueConstraint(LabelId label,
const std::set<PropertyId> &properties) = 0;
virtual bool UniqueConstraintExists(const LabelId &label, const PropertyId &property) const = 0;
protected:
Storage *storage_;
std::shared_lock<utils::ResourceLock> storage_guard_;

View File

@@ -10,6 +10,7 @@
// licenses/APL.txt.
#include "storage/v2/vertices_iterable.hpp"
#include "storage/v2/inmemory/unique_constraints.hpp"
namespace memgraph::storage {
@@ -26,6 +27,11 @@ VerticesIterable::VerticesIterable(InMemoryLabelPropertyIndex::Iterable vertices
new (&in_memory_vertices_by_label_property_) InMemoryLabelPropertyIndex::Iterable(std::move(vertices));
}
VerticesIterable::VerticesIterable(InMemoryUniqueConstraints::Iterable vertices)
: type_(Type::BY_UNIQUE_CONSTRAINT_IN_MEMORY) {
new (&in_memory_vertices_by_unique_constraint_) InMemoryUniqueConstraints::Iterable(std::move(vertices));
}
VerticesIterable::VerticesIterable(VerticesIterable &&other) noexcept : type_(other.type_) {
switch (other.type_) {
case Type::ALL:
@@ -38,6 +44,10 @@ VerticesIterable::VerticesIterable(VerticesIterable &&other) noexcept : type_(ot
new (&in_memory_vertices_by_label_property_)
InMemoryLabelPropertyIndex::Iterable(std::move(other.in_memory_vertices_by_label_property_));
break;
case Type::BY_UNIQUE_CONSTRAINT_IN_MEMORY:
new (&in_memory_vertices_by_unique_constraint_)
InMemoryUniqueConstraints::Iterable(std::move(other.in_memory_vertices_by_unique_constraint_));
break;
}
}
@@ -52,6 +62,9 @@ VerticesIterable &VerticesIterable::operator=(VerticesIterable &&other) noexcept
case Type::BY_LABEL_PROPERTY_IN_MEMORY:
in_memory_vertices_by_label_property_.InMemoryLabelPropertyIndex::Iterable::~Iterable();
break;
case Type::BY_UNIQUE_CONSTRAINT_IN_MEMORY:
in_memory_vertices_by_unique_constraint_.InMemoryUniqueConstraints::Iterable::~Iterable();
break;
}
type_ = other.type_;
switch (other.type_) {
@@ -65,6 +78,10 @@ VerticesIterable &VerticesIterable::operator=(VerticesIterable &&other) noexcept
new (&in_memory_vertices_by_label_property_)
InMemoryLabelPropertyIndex::Iterable(std::move(other.in_memory_vertices_by_label_property_));
break;
case Type::BY_UNIQUE_CONSTRAINT_IN_MEMORY:
new (&in_memory_vertices_by_unique_constraint_)
InMemoryUniqueConstraints::Iterable(std::move(other.in_memory_vertices_by_unique_constraint_));
break;
}
return *this;
}
@@ -80,6 +97,9 @@ VerticesIterable::~VerticesIterable() {
case Type::BY_LABEL_PROPERTY_IN_MEMORY:
in_memory_vertices_by_label_property_.InMemoryLabelPropertyIndex::Iterable::~Iterable();
break;
case Type::BY_UNIQUE_CONSTRAINT_IN_MEMORY:
in_memory_vertices_by_unique_constraint_.InMemoryUniqueConstraints::Iterable::~Iterable();
break;
}
}
@@ -91,6 +111,8 @@ VerticesIterable::Iterator VerticesIterable::begin() {
return Iterator(in_memory_vertices_by_label_.begin());
case Type::BY_LABEL_PROPERTY_IN_MEMORY:
return Iterator(in_memory_vertices_by_label_property_.begin());
case Type::BY_UNIQUE_CONSTRAINT_IN_MEMORY:
return Iterator(in_memory_vertices_by_unique_constraint_.begin());
}
}
@@ -102,6 +124,8 @@ VerticesIterable::Iterator VerticesIterable::end() {
return Iterator(in_memory_vertices_by_label_.end());
case Type::BY_LABEL_PROPERTY_IN_MEMORY:
return Iterator(in_memory_vertices_by_label_property_.end());
case Type::BY_UNIQUE_CONSTRAINT_IN_MEMORY:
return Iterator(in_memory_vertices_by_unique_constraint_.end());
}
}
@@ -121,6 +145,12 @@ VerticesIterable::Iterator::Iterator(InMemoryLabelPropertyIndex::Iterable::Itera
new (&in_memory_by_label_property_it_) InMemoryLabelPropertyIndex::Iterable::Iterator(std::move(it));
}
VerticesIterable::Iterator::Iterator(InMemoryUniqueConstraints::Iterable::Iterator it)
: type_(Type::BY_UNIQUE_CONSTRAINT_IN_MEMORY) {
// NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg)
new (&in_memory_by_unique_constraint_it_) InMemoryUniqueConstraints::Iterable::Iterator(std::move(it));
}
VerticesIterable::Iterator::Iterator(const VerticesIterable::Iterator &other) : type_(other.type_) {
switch (other.type_) {
case Type::ALL:
@@ -133,6 +163,10 @@ VerticesIterable::Iterator::Iterator(const VerticesIterable::Iterator &other) :
new (&in_memory_by_label_property_it_)
InMemoryLabelPropertyIndex::Iterable::Iterator(other.in_memory_by_label_property_it_);
break;
case Type::BY_UNIQUE_CONSTRAINT_IN_MEMORY:
new (&in_memory_by_unique_constraint_it_)
InMemoryUniqueConstraints::Iterable::Iterator(other.in_memory_by_unique_constraint_it_);
break;
}
}
@@ -151,6 +185,10 @@ VerticesIterable::Iterator &VerticesIterable::Iterator::operator=(const Vertices
new (&in_memory_by_label_property_it_)
InMemoryLabelPropertyIndex::Iterable::Iterator(other.in_memory_by_label_property_it_);
break;
case Type::BY_UNIQUE_CONSTRAINT_IN_MEMORY:
new (&in_memory_by_unique_constraint_it_)
InMemoryUniqueConstraints::Iterable::Iterator(other.in_memory_by_unique_constraint_it_);
break;
}
return *this;
}
@@ -170,6 +208,11 @@ VerticesIterable::Iterator::Iterator(VerticesIterable::Iterator &&other) noexcep
// NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg)
InMemoryLabelPropertyIndex::Iterable::Iterator(std::move(other.in_memory_by_label_property_it_));
break;
case Type::BY_UNIQUE_CONSTRAINT_IN_MEMORY:
new (&in_memory_by_unique_constraint_it_)
// NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg)
InMemoryUniqueConstraints::Iterable::Iterator(std::move(other.in_memory_by_unique_constraint_it_));
break;
}
}
@@ -190,6 +233,11 @@ VerticesIterable::Iterator &VerticesIterable::Iterator::operator=(VerticesIterab
// NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg)
InMemoryLabelPropertyIndex::Iterable::Iterator(std::move(other.in_memory_by_label_property_it_));
break;
case Type::BY_UNIQUE_CONSTRAINT_IN_MEMORY:
new (&in_memory_by_unique_constraint_it_)
// NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg)
InMemoryUniqueConstraints::Iterable::Iterator(std::move(other.in_memory_by_unique_constraint_it_));
break;
}
return *this;
}
@@ -207,6 +255,9 @@ void VerticesIterable::Iterator::Destroy() noexcept {
case Type::BY_LABEL_PROPERTY_IN_MEMORY:
in_memory_by_label_property_it_.InMemoryLabelPropertyIndex::Iterable::Iterator::~Iterator();
break;
case Type::BY_UNIQUE_CONSTRAINT_IN_MEMORY:
in_memory_by_unique_constraint_it_.InMemoryUniqueConstraints::Iterable::Iterator::~Iterator();
break;
}
}
@@ -218,6 +269,8 @@ VertexAccessor const &VerticesIterable::Iterator::operator*() const {
return *in_memory_by_label_it_;
case Type::BY_LABEL_PROPERTY_IN_MEMORY:
return *in_memory_by_label_property_it_;
case Type::BY_UNIQUE_CONSTRAINT_IN_MEMORY:
return *in_memory_by_unique_constraint_it_;
}
}
@@ -232,6 +285,9 @@ VerticesIterable::Iterator &VerticesIterable::Iterator::operator++() {
case Type::BY_LABEL_PROPERTY_IN_MEMORY:
++in_memory_by_label_property_it_;
break;
case Type::BY_UNIQUE_CONSTRAINT_IN_MEMORY:
++in_memory_by_unique_constraint_it_;
break;
}
return *this;
}
@@ -244,6 +300,8 @@ bool VerticesIterable::Iterator::operator==(const Iterator &other) const {
return in_memory_by_label_it_ == other.in_memory_by_label_it_;
case Type::BY_LABEL_PROPERTY_IN_MEMORY:
return in_memory_by_label_property_it_ == other.in_memory_by_label_property_it_;
case Type::BY_UNIQUE_CONSTRAINT_IN_MEMORY:
return in_memory_by_unique_constraint_it_ == other.in_memory_by_unique_constraint_it_;
}
}

View File

@@ -14,23 +14,26 @@
#include "storage/v2/all_vertices_iterable.hpp"
#include "storage/v2/inmemory/label_index.hpp"
#include "storage/v2/inmemory/label_property_index.hpp"
#include "storage/v2/inmemory/unique_constraints.hpp"
namespace memgraph::storage {
class VerticesIterable final {
enum class Type { ALL, BY_LABEL_IN_MEMORY, BY_LABEL_PROPERTY_IN_MEMORY };
enum class Type { ALL, BY_LABEL_IN_MEMORY, BY_LABEL_PROPERTY_IN_MEMORY, BY_UNIQUE_CONSTRAINT_IN_MEMORY };
Type type_;
union {
AllVerticesIterable all_vertices_;
InMemoryLabelIndex::Iterable in_memory_vertices_by_label_;
InMemoryLabelPropertyIndex::Iterable in_memory_vertices_by_label_property_;
InMemoryUniqueConstraints::Iterable in_memory_vertices_by_unique_constraint_;
};
public:
explicit VerticesIterable(AllVerticesIterable);
explicit VerticesIterable(InMemoryLabelIndex::Iterable);
explicit VerticesIterable(InMemoryLabelPropertyIndex::Iterable);
explicit VerticesIterable(InMemoryUniqueConstraints::Iterable);
VerticesIterable(const VerticesIterable &) = delete;
VerticesIterable &operator=(const VerticesIterable &) = delete;
@@ -46,6 +49,7 @@ class VerticesIterable final {
AllVerticesIterable::Iterator all_it_;
InMemoryLabelIndex::Iterable::Iterator in_memory_by_label_it_;
InMemoryLabelPropertyIndex::Iterable::Iterator in_memory_by_label_property_it_;
InMemoryUniqueConstraints::Iterable::Iterator in_memory_by_unique_constraint_it_;
};
void Destroy() noexcept;
@@ -54,6 +58,7 @@ class VerticesIterable final {
explicit Iterator(AllVerticesIterable::Iterator);
explicit Iterator(InMemoryLabelIndex::Iterable::Iterator);
explicit Iterator(InMemoryLabelPropertyIndex::Iterable::Iterator);
explicit Iterator(InMemoryUniqueConstraints::Iterable::Iterator);
Iterator(const Iterator &);
Iterator &operator=(const Iterator &);

View File

@@ -68,6 +68,7 @@ add_subdirectory(set_properties)
add_subdirectory(transaction_rollback)
add_subdirectory(query_modules)
add_subdirectory(constraints)
add_subdirectory(constraints_as_indices)
copy_e2e_python_files(pytest_runner pytest_runner.sh "")
copy_e2e_python_files(x x.sh "")

View File

@@ -0,0 +1,6 @@
function(copy_constraints_as_indices_e2e_python_files FILE_NAME)
copy_e2e_python_files(constraints_as_indices ${FILE_NAME})
endfunction()
copy_constraints_as_indices_e2e_python_files(common.py)
copy_constraints_as_indices_e2e_python_files(constraints_as_indices.py)

View File

@@ -0,0 +1,30 @@
# Copyright 2023 Memgraph Ltd.
#
# Use of this software is governed by the Business Source License
# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
# License, and you may not use this file except in compliance with the Business Source License.
#
# As of the Change Date specified in that file, in accordance with
# the Business Source License, use of this software will be governed
# by the Apache License, Version 2.0, included in the file
# licenses/APL.txt.
import pytest
from gqlalchemy import Memgraph
QUERY_PLAN = "QUERY PLAN"
@pytest.fixture
def memgraph(**kwargs) -> Memgraph:
memgraph = Memgraph()
yield memgraph
memgraph.drop_database()
memgraph.drop_indexes()
memgraph.ensure_constraints([])
def extract_query_plan(results):
return [x[QUERY_PLAN] for x in results]

View File

@@ -0,0 +1,69 @@
# Copyright 2023 Memgraph Ltd.
#
# Use of this software is governed by the Business Source License
# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
# License, and you may not use this file except in compliance with the Business Source License.
#
# As of the Change Date specified in that file, in accordance with
# the Business Source License, use of this software will be governed
# by the Apache License, Version 2.0, included in the file
# licenses/APL.txt.
import sys
import pytest
from common import extract_query_plan, memgraph
def test_given_constraint_when_querying_then_index_scanning(memgraph):
memgraph.execute("CREATE CONSTRAINT ON (n:label) ASSERT n.prop IS UNIQUE;")
expected_results = [" * Produce {n}", " * ScanAllByLabelPropertyValue (n :label {prop})", " * Once"]
actual_results = extract_query_plan(
memgraph.execute_and_fetch("EXPLAIN MATCH (n:label) WHERE n.prop = 1 RETURN n;")
)
assert expected_results == actual_results
def test_given_multiprop_constraint_when_querying_then_sequential_scanning(memgraph):
memgraph.execute("CREATE CONSTRAINT ON (n:label) ASSERT n.prop1, n.prop2 IS UNIQUE;")
expected_results = [" * Produce {n}", " * Filter", " * ScanAll (n)", " * Once"]
actual_results = extract_query_plan(
memgraph.execute_and_fetch("EXPLAIN MATCH (n:label) WHERE n.prop1 = 1 AND n.prop2 = 2 RETURN n;")
)
assert expected_results == actual_results
def test_given_constraint_and_index_when_querying_then_index_scanning(memgraph):
memgraph.execute("CREATE CONSTRAINT ON (n:label) ASSERT n.prop1 IS UNIQUE;")
memgraph.execute("CREATE INDEX ON :label(prop1);")
expected_results = [" * Produce {n}", " * ScanAllByLabelPropertyValue (n :label {prop1})", " * Once"]
actual_results = extract_query_plan(
memgraph.execute_and_fetch("EXPLAIN MATCH (n:label) WHERE n.prop1 = 1 RETURN n;")
)
assert expected_results == actual_results
def test_given_constraint_and_index_with_different_distribution_when_querying_then_prefer_constraint(memgraph):
memgraph.execute("CREATE CONSTRAINT ON (n:label) ASSERT n.prop1 IS UNIQUE;")
memgraph.execute("CREATE INDEX ON :label(prop2);")
memgraph.execute("FOREACH (i IN range(1, 1000) | CREATE (:Node {prop1: i, prop2: i % 2}))")
expected_results = [" * Produce {n}", " * Filter", " * ScanAllByLabelPropertyValue (n :label {prop1})", " * Once"]
actual_results = extract_query_plan(
memgraph.execute_and_fetch("EXPLAIN MATCH (n:label) WHERE n.prop1 = 500 and n.prop2 = 0 RETURN n;")
)
assert expected_results == actual_results
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-rA"]))

View File

@@ -0,0 +1,14 @@
constraints_as_indices_cluster: &constraints_as_indices_cluster
cluster:
main:
args: ["--bolt-port", "7687", "--log-level=TRACE"]
log_file: "analyze_graph.log"
setup_queries: []
validation_queries: []
workloads:
- name: "Constraints as indices"
binary: "tests/e2e/pytest_runner.sh"
args: ["analyze_graph/constraints_as_indices.py"]
<<: *constraints_as_indices_cluster

View File

@@ -213,6 +213,11 @@ class InteractiveDbAccessor {
return label_property_index_.at(key);
}
bool UniqueConstraintExists(const memgraph::storage::LabelId &label_id,
const memgraph::storage::PropertyId &property_id) {
return true;
}
std::optional<memgraph::storage::LabelIndexStats> GetIndexStats(const memgraph::storage::LabelId label) const {
return dba_->GetIndexStats(label);
}
@@ -222,6 +227,10 @@ class InteractiveDbAccessor {
return dba_->GetIndexStats(label, property);
}
bool IndexedScanExists(const memgraph::storage::LabelId &label, const memgraph::storage::PropertyId &prop) const {
return dba_->IndexedScanExists(label, prop);
}
// Save the cached vertex counts to a stream.
void Save(std::ostream &out) {
out << "vertex-count " << vertices_count_ << std::endl;

View File

@@ -1144,6 +1144,20 @@ TYPED_TEST(TestPlanner, WhereIndexedLabelProperty) {
CheckPlan(planner.plan(), symbol_table, ExpectScanAllByLabelPropertyValue(label, property, lit_42), ExpectProduce());
}
TYPED_TEST(TestPlanner, WhereIndexedUniqueConstraint) {
// Test MATCH (n :label) WHERE n.property = 42 RETURN n
FakeDbAccessor dba;
auto label = dba.Label("label");
auto property = PROPERTY_PAIR(dba, "property");
dba.SetConstraintCount(label, property.second, 0);
auto lit_42 = LITERAL(42);
auto *query = QUERY(SINGLE_QUERY(MATCH(PATTERN(NODE("n", "label"))),
WHERE(EQ(PROPERTY_LOOKUP(dba, "n", property), lit_42)), RETURN("n")));
auto symbol_table = memgraph::query::MakeSymbolTable(query);
auto planner = MakePlanner<TypeParam>(&dba, this->storage, symbol_table, query);
CheckPlan(planner.plan(), symbol_table, ExpectScanAllByLabelPropertyValue(label, property, lit_42), ExpectProduce());
}
TYPED_TEST(TestPlanner, BestPropertyIndexed) {
// Test MATCH (n :label) WHERE n.property = 1 AND n.better = 42 RETURN n
FakeDbAccessor dba;
@@ -1165,6 +1179,27 @@ TYPED_TEST(TestPlanner, BestPropertyIndexed) {
ExpectProduce());
}
TYPED_TEST(TestPlanner, BetterConstraintIndexedThanLabelPropertyIndex) {
// Test MATCH (n :label) WHERE n.property = 1 AND n.better = 42 RETURN n
FakeDbAccessor dba;
auto label = dba.Label("label");
auto property = dba.Property("property");
// Add a vertex with :label+property combination, so that the best
// :label+better remains empty and thus better choice.
dba.SetIndexCount(label, property, 1);
auto better = PROPERTY_PAIR(dba, "better");
dba.SetConstraintCount(label, better.second, 0);
auto lit_42 = LITERAL(42);
auto *query = QUERY(SINGLE_QUERY(
MATCH(PATTERN(NODE("n", "label"))),
WHERE(AND(EQ(PROPERTY_LOOKUP(dba, "n", property), LITERAL(1)), EQ(PROPERTY_LOOKUP(dba, "n", better), lit_42))),
RETURN("n")));
auto symbol_table = memgraph::query::MakeSymbolTable(query);
auto planner = MakePlanner<TypeParam>(&dba, this->storage, symbol_table, query);
CheckPlan(planner.plan(), symbol_table, ExpectScanAllByLabelPropertyValue(label, better, lit_42), ExpectFilter(),
ExpectProduce());
}
TYPED_TEST(TestPlanner, MultiPropertyIndexScan) {
// Test MATCH (n :label1), (m :label2) WHERE n.prop1 = 1 AND m.prop2 = 2
// RETURN n, m

View File

@@ -520,9 +520,11 @@ TPlanner MakePlanner(TDbAccessor *dba, AstStorage &storage, SymbolTable &symbol_
class FakeDbAccessor {
public:
int64_t VerticesCount(memgraph::storage::LabelId label) const {
auto found = label_index_.find(label);
if (found != label_index_.end()) return found->second;
return 0;
if (!LabelIndexExists(label)) {
return 0;
}
return label_index_.find(label)->second;
}
int64_t VerticesCount(memgraph::storage::LabelId label, memgraph::storage::PropertyId property) const {
@@ -531,6 +533,44 @@ class FakeDbAccessor {
return std::get<2>(index);
}
}
for (auto const &unique_constraint : unique_constraints_) {
if (std::get<0>(unique_constraint) != label) {
continue;
}
auto const &props = std::get<1>(unique_constraint);
if (props.size() != 1) {
continue;
}
return std::get<2>(unique_constraint);
}
return 0;
}
int64_t VerticesCount(const memgraph::storage::LabelId &label, const memgraph::storage::PropertyId &property,
const memgraph::storage::PropertyValue &value) const {
for (auto &index : label_property_index_) {
if (std::get<0>(index) == label && std::get<1>(index) == property) {
return std::get<2>(index);
}
}
for (auto const &unique_constraint : unique_constraints_) {
if (std::get<0>(unique_constraint) != label) {
continue;
}
auto const &props = std::get<1>(unique_constraint);
if (props.size() != 1) {
continue;
}
return std::get<2>(unique_constraint);
}
return 0;
}
@@ -538,6 +578,27 @@ class FakeDbAccessor {
return label_index_.find(label) != label_index_.end();
}
bool UniqueConstraintExists(const memgraph::storage::LabelId &label,
const memgraph::storage::PropertyId &property) const {
for (auto const &unique_constraint : unique_constraints_) {
if (std::get<0>(unique_constraint) != label) {
continue;
}
auto const &props = std::get<1>(unique_constraint);
if (props.size() != 1) {
continue;
}
return props[0] == property;
}
return false;
}
bool IndexedScanExists(const memgraph::storage::LabelId &label, const memgraph::storage::PropertyId &prop) const {
return LabelPropertyIndexExists(label, prop) || UniqueConstraintExists(label, prop);
}
bool LabelPropertyIndexExists(memgraph::storage::LabelId label, memgraph::storage::PropertyId property) const {
for (auto &index : label_property_index_) {
if (std::get<0>(index) == label && std::get<1>(index) == property) {
@@ -568,6 +629,28 @@ class FakeDbAccessor {
label_property_index_.emplace_back(label, property, count);
}
void SetConstraintCount(memgraph::storage::LabelId label, memgraph::storage::PropertyId property, int64_t count) {
for (auto &constraint : unique_constraints_) {
if (std::get<0>(constraint) != label) {
continue;
}
auto const &props = std::get<1>(constraint);
if (props.size() != 1) {
continue;
}
if (props[0] != property) {
continue;
}
std::get<2>(constraint) = count;
return;
}
std::vector<memgraph::storage::PropertyId> props{property};
unique_constraints_.emplace_back(label, props, count);
}
memgraph::storage::LabelId NameToLabel(const std::string &name) {
auto found = labels_.find(name);
if (found != labels_.end()) return found->second;
@@ -606,6 +689,8 @@ class FakeDbAccessor {
std::unordered_map<memgraph::storage::LabelId, int64_t> label_index_;
std::vector<std::tuple<memgraph::storage::LabelId, memgraph::storage::PropertyId, int64_t>> label_property_index_;
std::vector<std::tuple<memgraph::storage::LabelId, std::vector<memgraph::storage::PropertyId>, int64_t>>
unique_constraints_;
};
} // namespace memgraph::query::plan

View File

@@ -4071,3 +4071,79 @@ TYPED_TEST(SubqueriesFeature, SubqueriesWithForeach) {
auto results = CollectProduce(*produce, &context);
EXPECT_EQ(results.size(), 2);
}
template <typename StorageType>
class QueryPlanConstraintsAsIndices : public testing::Test {
public:
memgraph::storage::Config config;
std::unique_ptr<memgraph::storage::Storage> db{new StorageType(config)};
AstStorage storage;
};
using InMemoryOnlyStorageType = ::testing::Types<memgraph::storage::InMemoryStorage>;
TYPED_TEST_CASE(QueryPlanConstraintsAsIndices, InMemoryOnlyStorageType);
/// @brief Test checks that adding a unique constraint can be used as an index when iterating over
/// vertices in an indexed manner
TYPED_TEST(QueryPlanConstraintsAsIndices, ScanByLabelPropertyUsesConstraints) {
auto label = this->db->NameToLabel("label");
auto prop = this->db->NameToProperty("prop");
// Add a few nodes with different properties
{
auto storage_dba = this->db->Access();
memgraph::query::DbAccessor dba(storage_dba.get());
for (int i = 0; i < 5; i++) {
auto vertex = dba.InsertVertex();
ASSERT_TRUE(vertex.AddLabel(label).HasValue());
ASSERT_TRUE(vertex.SetProperty(prop, memgraph::storage::PropertyValue(i)).HasValue());
}
ASSERT_FALSE(dba.Commit().HasError());
}
// Create unique constraint for those nodes
{
auto unique_acc = this->db->UniqueAccess();
[[maybe_unused]] auto _ = unique_acc->CreateUniqueConstraint(label, {prop});
ASSERT_FALSE(unique_acc->Commit().HasError());
}
// Iterate and verify returned nodes from the ranged indexed operator
{
auto storage_dba = this->db->Access();
memgraph::query::DbAccessor dba(storage_dba.get());
EXPECT_EQ(5, CountIterable(dba.Vertices(memgraph::storage::View::OLD)));
auto run_scan_all = [&](int lower, int upper) {
SymbolTable symbol_table;
auto scan_all = MakeScanAllByLabelPropertyRange(this->storage, symbol_table, "n", label, prop, "prop",
Bound{LITERAL(lower), Bound::Type::INCLUSIVE},
Bound{LITERAL(upper), Bound::Type::EXCLUSIVE});
auto output = NEXPR("n", IDENT("n")->MapTo(scan_all.sym_))->MapTo(symbol_table.CreateSymbol("n", true));
auto produce = MakeProduce(scan_all.op_, output);
auto context = MakeContext(this->storage, symbol_table, &dba);
auto results = CollectProduce(*produce, &context);
ASSERT_EQ(results.size(), std::max(upper - lower, 0));
for (int i = lower, j = 0; i < upper; i++, j++) {
const auto &row = results[j];
ASSERT_EQ(row.size(), 1);
auto vertex = row[0].ValueVertex();
TypedValue value(*vertex.GetProperty(memgraph::storage::View::OLD, prop));
ASSERT_TRUE(value.IsInt());
ASSERT_EQ(value.ValueInt(), i);
}
};
run_scan_all(1, 1);
run_scan_all(1, 2);
run_scan_all(1, 3);
run_scan_all(1, 4);
run_scan_all(1, 5);
run_scan_all(5, 5);
run_scan_all(4, 5);
run_scan_all(3, 5);
run_scan_all(2, 5);
run_scan_all(1, 5);
}
}