Compare commits
15 Commits
run-releas
...
use-constr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2df6db50c0 | ||
|
|
bda353b23c | ||
|
|
d04fef8ce0 | ||
|
|
c83c924c22 | ||
|
|
1ea78ca058 | ||
|
|
d399b86479 | ||
|
|
0f27444ec4 | ||
|
|
10ace3f445 | ||
|
|
c56be187cc | ||
|
|
d908ce76cd | ||
|
|
ab40115b1e | ||
|
|
edfa183ff7 | ||
|
|
33310b5800 | ||
|
|
78833afd41 | ||
|
|
aed6a2de24 |
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 ¶meters)
|
||||
: 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 ¶meters_;
|
||||
// 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 ¶meters) {
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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_;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 &);
|
||||
|
||||
@@ -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 "")
|
||||
|
||||
6
tests/e2e/constraints_as_indices/CMakeLists.txt
Normal file
6
tests/e2e/constraints_as_indices/CMakeLists.txt
Normal 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)
|
||||
30
tests/e2e/constraints_as_indices/common.py
Normal file
30
tests/e2e/constraints_as_indices/common.py
Normal 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]
|
||||
69
tests/e2e/constraints_as_indices/constraints_as_indices.py
Normal file
69
tests/e2e/constraints_as_indices/constraints_as_indices.py
Normal 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"]))
|
||||
14
tests/e2e/constraints_as_indices/workloads.yaml
Normal file
14
tests/e2e/constraints_as_indices/workloads.yaml
Normal 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user