Plan scanning by label & property index for node atoms
Summary: Add Filters class for storing additional info Add FindOr to utils/algorithm.hpp Use all collected labels when scanning by them Collect label filters inside WHERE Document the Filters class Reviewers: florijan, mislav.bradac, buda Reviewed By: florijan Subscribers: pullbot, lion Differential Revision: https://phabricator.memgraph.io/D515
This commit is contained in:
parent
051c38acab
commit
d9a724f0cd
@ -303,7 +303,7 @@ class GraphDbAccessor {
|
||||
* is ready for use.
|
||||
*/
|
||||
bool LabelPropertyIndexExists(const GraphDbTypes::Label &label,
|
||||
const GraphDbTypes::Property &property) {
|
||||
const GraphDbTypes::Property &property) const {
|
||||
return db_.label_property_index_.IndexExists(
|
||||
LabelPropertyIndex::Key(label, property));
|
||||
}
|
||||
|
@ -26,7 +26,54 @@ struct Expansion {
|
||||
NodeAtom *node2 = nullptr;
|
||||
};
|
||||
|
||||
/// @brief Normalized representation of a single or multiple Match clauses.
|
||||
/// Stores information on filters used inside the @c Matching of a @c QueryPart.
|
||||
class Filters {
|
||||
public:
|
||||
/// Stores the symbols and expression used to filter a property.
|
||||
struct PropertyFilter {
|
||||
/// Set of used symbols in the @c expression.
|
||||
std::unordered_set<Symbol> used_symbols;
|
||||
/// Expression which when evaluated produces the value a property must
|
||||
/// equal.
|
||||
Expression *expression;
|
||||
};
|
||||
|
||||
/// All filter expressions that should be generated.
|
||||
auto &all_filters() { return all_filters_; }
|
||||
const auto &all_filters() const { return all_filters_; }
|
||||
/// Mapping from a symbol to labels that are filtered on it. These should be
|
||||
/// used only for generating indexed scans.
|
||||
const auto &label_filters() const { return label_filters_; }
|
||||
/// Mapping from a symbol to properties that are filtered on it. These should
|
||||
/// be used only for generating indexed scans.
|
||||
const auto &property_filters() const { return property_filters_; }
|
||||
|
||||
/// Collects filtering information from a pattern.
|
||||
///
|
||||
/// Goes through all the atoms in a pattern and generates filter expressions
|
||||
/// for found labels, properties and edge types. The generated expressions are
|
||||
/// stored in @c all_filters. Also, @c label_filters and @c property_filters
|
||||
/// are populated.
|
||||
void CollectPatternFilters(Pattern &, const SymbolTable &, AstTreeStorage &);
|
||||
/// Collects filtering information from a where expression.
|
||||
///
|
||||
/// Takes the where expression and stores it in @c all_filters, then analyzes
|
||||
/// the expression for additional information. The additional information is
|
||||
/// used to populate @c label_filters and @c property_filters, so that indexed
|
||||
/// scanning can use it.
|
||||
void CollectWhereFilter(Where &, const SymbolTable &);
|
||||
|
||||
private:
|
||||
void AnalyzeFilter(Expression *, const SymbolTable &);
|
||||
|
||||
std::vector<std::pair<Expression *, std::unordered_set<Symbol>>> all_filters_;
|
||||
std::unordered_map<Symbol, std::set<GraphDbTypes::Label>> label_filters_;
|
||||
std::unordered_map<
|
||||
Symbol, std::map<GraphDbTypes::Property, std::vector<PropertyFilter>>>
|
||||
property_filters_;
|
||||
};
|
||||
|
||||
/// Normalized representation of a single or multiple Match clauses.
|
||||
///
|
||||
/// For example, `MATCH (a :Label) -[e1]- (b) -[e2]- (c) MATCH (n) -[e3]- (m)
|
||||
/// WHERE c.prop < 42` will produce the following.
|
||||
@ -37,16 +84,15 @@ struct Expansion {
|
||||
/// Filters will contain 2 pairs. One for testing `:Label` on symbol `a` and the
|
||||
/// other obtained from `WHERE` on symbol `c`.
|
||||
struct Matching {
|
||||
/// @brief All expansions that need to be performed across @c Match clauses.
|
||||
/// All expansions that need to be performed across @c Match clauses.
|
||||
std::vector<Expansion> expansions;
|
||||
/// @brief Symbols for edges established in match, used to ensure
|
||||
/// Cyphermorphism.
|
||||
/// Symbols for edges established in match, used to ensure Cyphermorphism.
|
||||
///
|
||||
/// There are multiple sets, because each Match clause determines a single
|
||||
/// set.
|
||||
std::vector<std::unordered_set<Symbol>> edge_symbols;
|
||||
/// @brief Pairs of filter expression and symbols used in them.
|
||||
std::vector<std::pair<Expression *, std::unordered_set<Symbol>>> filters;
|
||||
/// Information on used filter expressions while matching.
|
||||
Filters filters;
|
||||
};
|
||||
|
||||
/// @brief Represents a read (+ write) part of a query. Parts are split on
|
||||
|
@ -2,9 +2,11 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "query/frontend/ast/ast.hpp"
|
||||
#include "utils/algorithm.hpp"
|
||||
#include "utils/exceptions.hpp"
|
||||
|
||||
namespace query::plan {
|
||||
@ -166,59 +168,6 @@ Expression *BoolJoin(AstTreeStorage &storage, Expression *expr1,
|
||||
return expr1 ? expr1 : expr2;
|
||||
}
|
||||
|
||||
template <class TAtom>
|
||||
Expression *PropertiesEqual(AstTreeStorage &storage,
|
||||
UsedSymbolsCollector &collector, TAtom *atom) {
|
||||
Expression *filter_expr = nullptr;
|
||||
for (auto &prop_pair : atom->properties_) {
|
||||
prop_pair.second->Accept(collector);
|
||||
auto *property_lookup =
|
||||
storage.Create<PropertyLookup>(atom->identifier_, prop_pair.first);
|
||||
auto *prop_equal =
|
||||
storage.Create<EqualOperator>(property_lookup, prop_pair.second);
|
||||
filter_expr = BoolJoin<FilterAndOperator>(storage, filter_expr, prop_equal);
|
||||
}
|
||||
return filter_expr;
|
||||
}
|
||||
|
||||
void CollectPatternFilters(
|
||||
Pattern &pattern, const SymbolTable &symbol_table,
|
||||
std::vector<std::pair<Expression *, std::unordered_set<Symbol>>> &filters,
|
||||
AstTreeStorage &storage) {
|
||||
UsedSymbolsCollector collector(symbol_table);
|
||||
auto node_filter = [&](NodeAtom *node) {
|
||||
Expression *labels_filter =
|
||||
node->labels_.empty() ? nullptr : storage.Create<LabelsTest>(
|
||||
node->identifier_, node->labels_);
|
||||
auto *props_filter = PropertiesEqual(storage, collector, node);
|
||||
if (labels_filter || props_filter) {
|
||||
collector.symbols_.insert(symbol_table.at(*node->identifier_));
|
||||
filters.emplace_back(
|
||||
BoolJoin<FilterAndOperator>(storage, labels_filter, props_filter),
|
||||
collector.symbols_);
|
||||
collector.symbols_.clear();
|
||||
}
|
||||
};
|
||||
auto expand_filter = [&](NodeAtom *prev_node, EdgeAtom *edge,
|
||||
NodeAtom *node) {
|
||||
Expression *types_filter = edge->edge_types_.empty()
|
||||
? nullptr
|
||||
: storage.Create<EdgeTypeTest>(
|
||||
edge->identifier_, edge->edge_types_);
|
||||
auto *props_filter = PropertiesEqual(storage, collector, edge);
|
||||
if (types_filter || props_filter) {
|
||||
const auto &edge_symbol = symbol_table.at(*edge->identifier_);
|
||||
collector.symbols_.insert(edge_symbol);
|
||||
filters.emplace_back(
|
||||
BoolJoin<FilterAndOperator>(storage, types_filter, props_filter),
|
||||
collector.symbols_);
|
||||
collector.symbols_.clear();
|
||||
}
|
||||
node_filter(node);
|
||||
};
|
||||
ForeachPattern(pattern, node_filter, expand_filter);
|
||||
}
|
||||
|
||||
// Contextual information used for generating match operators.
|
||||
struct MatchContext {
|
||||
const SymbolTable &symbol_table;
|
||||
@ -232,16 +181,18 @@ struct MatchContext {
|
||||
std::vector<Symbol> new_symbols;
|
||||
};
|
||||
|
||||
auto GenFilters(
|
||||
LogicalOperator *last_op, const std::unordered_set<Symbol> &bound_symbols,
|
||||
std::vector<std::pair<Expression *, std::unordered_set<Symbol>>> &filters,
|
||||
AstTreeStorage &storage) {
|
||||
auto GenFilters(LogicalOperator *last_op,
|
||||
const std::unordered_set<Symbol> &bound_symbols,
|
||||
std::vector<std::pair<Expression *, std::unordered_set<Symbol>>>
|
||||
&all_filters,
|
||||
AstTreeStorage &storage) {
|
||||
Expression *filter_expr = nullptr;
|
||||
for (auto filters_it = filters.begin(); filters_it != filters.end();) {
|
||||
for (auto filters_it = all_filters.begin();
|
||||
filters_it != all_filters.end();) {
|
||||
if (HasBoundFilterSymbols(bound_symbols, *filters_it)) {
|
||||
filter_expr =
|
||||
BoolJoin<FilterAndOperator>(storage, filter_expr, filters_it->first);
|
||||
filters_it = filters.erase(filters_it);
|
||||
filters_it = all_filters.erase(filters_it);
|
||||
} else {
|
||||
filters_it++;
|
||||
}
|
||||
@ -691,12 +642,10 @@ void AddMatching(const std::vector<Pattern *> &patterns, Where *where,
|
||||
matching.expansions.insert(matching.expansions.end(), expansions.begin(),
|
||||
expansions.end());
|
||||
for (auto *pattern : patterns) {
|
||||
CollectPatternFilters(*pattern, symbol_table, matching.filters, storage);
|
||||
matching.filters.CollectPatternFilters(*pattern, symbol_table, storage);
|
||||
}
|
||||
if (where) {
|
||||
UsedSymbolsCollector collector(symbol_table);
|
||||
where->expression_->Accept(collector);
|
||||
matching.filters.emplace_back(where->expression_, collector.symbols_);
|
||||
matching.filters.CollectWhereFilter(*where, symbol_table);
|
||||
}
|
||||
}
|
||||
void AddMatching(const Match &match, const SymbolTable &symbol_table,
|
||||
@ -706,13 +655,13 @@ void AddMatching(const Match &match, const SymbolTable &symbol_table,
|
||||
}
|
||||
|
||||
const GraphDbTypes::Label &FindBestLabelIndex(
|
||||
const std::vector<GraphDbTypes::Label> &labels, const GraphDbAccessor *db) {
|
||||
const GraphDbAccessor *db, const std::set<GraphDbTypes::Label> &labels) {
|
||||
debug_assert(!labels.empty(),
|
||||
"Trying to find the best label without any labels.");
|
||||
if (!db) {
|
||||
// We don't have a database to get index information, so just take the first
|
||||
// label.
|
||||
return labels.front();
|
||||
return *labels.begin();
|
||||
}
|
||||
return *std::min_element(labels.begin(), labels.end(),
|
||||
[db](const auto &label1, const auto &label2) {
|
||||
@ -721,6 +670,63 @@ const GraphDbTypes::Label &FindBestLabelIndex(
|
||||
});
|
||||
}
|
||||
|
||||
// Finds the label-property combination which has indexed the lowest amount of
|
||||
// vertices. `best_label` and `best_property` will be set to that combination
|
||||
// and the function will return `true`. If the index cannot be found, the
|
||||
// function will return `false` while leaving `best_label` and `best_property`
|
||||
// unchanged.
|
||||
bool FindBestLabelPropertyIndex(
|
||||
const GraphDbAccessor *db, const std::set<GraphDbTypes::Label> &labels,
|
||||
const std::map<GraphDbTypes::Property, std::vector<Filters::PropertyFilter>>
|
||||
&property_filters,
|
||||
const Symbol &symbol, const std::unordered_set<Symbol> &bound_symbols,
|
||||
GraphDbTypes::Label &best_label,
|
||||
std::pair<GraphDbTypes::Property, Filters::PropertyFilter> &best_property) {
|
||||
if (!db) {
|
||||
// Without the database, we cannot determine whether the index even exists.
|
||||
return false;
|
||||
}
|
||||
auto are_bound = [&bound_symbols](const auto &used_symbols) {
|
||||
for (const auto &used_symbol : used_symbols) {
|
||||
if (bound_symbols.find(used_symbol) == bound_symbols.end()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
bool found = false;
|
||||
size_t min_count = std::numeric_limits<size_t>::max();
|
||||
for (const auto &label : labels) {
|
||||
for (const auto &prop_pair : property_filters) {
|
||||
const auto &property = prop_pair.first;
|
||||
if (db->LabelPropertyIndexExists(label, property)) {
|
||||
auto vertices_count = db->vertices_count(label, property);
|
||||
if (vertices_count < min_count) {
|
||||
for (const auto &prop_filter : prop_pair.second) {
|
||||
if (prop_filter.used_symbols.find(symbol) !=
|
||||
prop_filter.used_symbols.end()) {
|
||||
// Skip filter expressions which use the symbol whose property we
|
||||
// are looking up. We cannot scan by such expressions. For
|
||||
// example, in `n.a = 2 + n.b` both sides of `=` refer to `n`, so
|
||||
// we cannot scan `n` by property index.
|
||||
continue;
|
||||
}
|
||||
if (are_bound(prop_filter.used_symbols)) {
|
||||
// Take the first property filter which uses bound symbols.
|
||||
best_label = label;
|
||||
best_property = {property, prop_filter};
|
||||
min_count = vertices_count;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
LogicalOperator *PlanMatching(const Matching &matching,
|
||||
LogicalOperator *input_op,
|
||||
PlanningContext &planning_ctx,
|
||||
@ -728,27 +734,51 @@ LogicalOperator *PlanMatching(const Matching &matching,
|
||||
auto &bound_symbols = context.bound_symbols;
|
||||
auto &storage = planning_ctx.ast_storage;
|
||||
const auto &symbol_table = context.symbol_table;
|
||||
// Copy filters, because we will modify the list as we generate Filters.
|
||||
auto filters = matching.filters;
|
||||
// Copy all_filters, because we will modify the list as we generate Filters.
|
||||
auto all_filters = matching.filters.all_filters();
|
||||
// Try to generate any filters even before the 1st match operator. This
|
||||
// optimizes the optional match which filters only on symbols bound in regular
|
||||
// match.
|
||||
auto *last_op = GenFilters(input_op, bound_symbols, filters, storage);
|
||||
auto *last_op = GenFilters(input_op, bound_symbols, all_filters, storage);
|
||||
for (const auto &expansion : matching.expansions) {
|
||||
const auto &node1_symbol = symbol_table.at(*expansion.node1->identifier_);
|
||||
if (BindSymbol(bound_symbols, node1_symbol)) {
|
||||
// We have just bound this symbol, so generate ScanAll which fills it.
|
||||
const auto &labels = expansion.node1->labels_;
|
||||
auto labels = FindOr(matching.filters.label_filters(), node1_symbol,
|
||||
std::set<GraphDbTypes::Label>())
|
||||
.first;
|
||||
if (labels.empty()) {
|
||||
// Without labels, we can only generate ScanAll of everything.
|
||||
last_op = new ScanAll(std::shared_ptr<LogicalOperator>(last_op),
|
||||
node1_symbol, context.graph_view);
|
||||
} else {
|
||||
auto label = FindBestLabelIndex(labels, planning_ctx.db);
|
||||
last_op = new ScanAllByLabel(std::shared_ptr<LogicalOperator>(last_op),
|
||||
node1_symbol, label, context.graph_view);
|
||||
// With labels, we can scan indexed data. First, try to see if we can
|
||||
// use label+property index. If not, use just the label index (which
|
||||
// ought to exist).
|
||||
GraphDbTypes::Label best_label;
|
||||
std::pair<GraphDbTypes::Property, Filters::PropertyFilter>
|
||||
best_property;
|
||||
auto properties =
|
||||
FindOr(matching.filters.property_filters(), node1_symbol,
|
||||
std::map<GraphDbTypes::Property,
|
||||
std::vector<Filters::PropertyFilter>>())
|
||||
.first;
|
||||
if (FindBestLabelPropertyIndex(planning_ctx.db, labels, properties,
|
||||
node1_symbol, bound_symbols, best_label,
|
||||
best_property)) {
|
||||
last_op = new ScanAllByLabelPropertyValue(
|
||||
std::shared_ptr<LogicalOperator>(last_op), node1_symbol,
|
||||
best_label, best_property.first, best_property.second.expression,
|
||||
context.graph_view);
|
||||
} else {
|
||||
auto label = FindBestLabelIndex(planning_ctx.db, labels);
|
||||
last_op =
|
||||
new ScanAllByLabel(std::shared_ptr<LogicalOperator>(last_op),
|
||||
node1_symbol, label, context.graph_view);
|
||||
}
|
||||
}
|
||||
context.new_symbols.emplace_back(node1_symbol);
|
||||
last_op = GenFilters(last_op, bound_symbols, filters, storage);
|
||||
last_op = GenFilters(last_op, bound_symbols, all_filters, storage);
|
||||
}
|
||||
// We have an edge, so generate Expand.
|
||||
if (expansion.edge) {
|
||||
@ -795,10 +825,10 @@ LogicalOperator *PlanMatching(const Matching &matching,
|
||||
}
|
||||
}
|
||||
}
|
||||
last_op = GenFilters(last_op, bound_symbols, filters, storage);
|
||||
last_op = GenFilters(last_op, bound_symbols, all_filters, storage);
|
||||
}
|
||||
}
|
||||
debug_assert(filters.empty(), "Expected to generate all filters");
|
||||
debug_assert(all_filters.empty(), "Expected to generate all filters");
|
||||
return last_op;
|
||||
}
|
||||
|
||||
@ -830,6 +860,116 @@ auto GenMerge(query::Merge &merge, LogicalOperator *input_op,
|
||||
|
||||
} // namespace
|
||||
|
||||
// Analyzes the filter expression by collecting information on filtering labels
|
||||
// and properties to be used with indexing. Note that all filters are never
|
||||
// updated here, but only labels and properties are.
|
||||
void Filters::AnalyzeFilter(Expression *expr, const SymbolTable &symbol_table) {
|
||||
auto get_property_lookup = [](auto *maybe_lookup, auto *&prop_lookup,
|
||||
auto *&ident) {
|
||||
return (prop_lookup = dynamic_cast<PropertyLookup *>(maybe_lookup)) &&
|
||||
(ident = dynamic_cast<Identifier *>(prop_lookup->expression_));
|
||||
};
|
||||
auto add_prop_equal = [&](auto *maybe_lookup, auto *val_expr) {
|
||||
PropertyLookup *prop_lookup = nullptr;
|
||||
Identifier *ident = nullptr;
|
||||
if (get_property_lookup(maybe_lookup, prop_lookup, ident)) {
|
||||
UsedSymbolsCollector collector(symbol_table);
|
||||
val_expr->Accept(collector);
|
||||
property_filters_[symbol_table.at(*ident)][prop_lookup->property_]
|
||||
.emplace_back(PropertyFilter{collector.symbols_, val_expr});
|
||||
}
|
||||
};
|
||||
// We are only interested to see the insides of And, because Or prevents
|
||||
// indexing since any labels and properties found there may be optional.
|
||||
if (auto *and_op = dynamic_cast<AndOperator *>(expr)) {
|
||||
AnalyzeFilter(and_op->expression1_, symbol_table);
|
||||
AnalyzeFilter(and_op->expression2_, symbol_table);
|
||||
} else if (auto *labels_test = dynamic_cast<LabelsTest *>(expr)) {
|
||||
// Since LabelsTest may contain any expression, we can only use the
|
||||
// simplest test on an identifier.
|
||||
if (auto *ident = dynamic_cast<Identifier *>(labels_test->expression_)) {
|
||||
const auto &symbol = symbol_table.at(*ident);
|
||||
label_filters_[symbol].insert(labels_test->labels_.begin(),
|
||||
labels_test->labels_.end());
|
||||
}
|
||||
} else if (auto *eq = dynamic_cast<EqualOperator *>(expr)) {
|
||||
// Try to get property equality test from the top expressions.
|
||||
// Unfortunately, we cannot go deeper inside Equal, because chained equals
|
||||
// need not correspond to And. For example, `(n.prop = value) = false)`:
|
||||
// EQ
|
||||
// / \
|
||||
// EQ false -- top expressions
|
||||
// / \
|
||||
// n.prop value
|
||||
// Here the `prop` may be different than `value` resulting in `false`. This
|
||||
// would compare with the top level `false`, producing `true`. Therefore, it
|
||||
// is incorrect to pick up `n.prop = value` for scanning by property index.
|
||||
add_prop_equal(eq->expression1_, eq->expression2_);
|
||||
// And reversed.
|
||||
add_prop_equal(eq->expression2_, eq->expression1_);
|
||||
}
|
||||
// TODO: Collect potential property indexing by range.
|
||||
return;
|
||||
}
|
||||
|
||||
void Filters::CollectPatternFilters(Pattern &pattern,
|
||||
const SymbolTable &symbol_table,
|
||||
AstTreeStorage &storage) {
|
||||
UsedSymbolsCollector collector(symbol_table);
|
||||
auto add_properties_filter = [&](auto *atom) {
|
||||
const auto &symbol = symbol_table.at(*atom->identifier_);
|
||||
for (auto &prop_pair : atom->properties_) {
|
||||
collector.symbols_.clear();
|
||||
prop_pair.second->Accept(collector);
|
||||
// Store a PropertyFilter on the value of the property.
|
||||
property_filters_[symbol][prop_pair.first].emplace_back(
|
||||
PropertyFilter{collector.symbols_, prop_pair.second});
|
||||
// Create an equality expression and store it in all_filters_.
|
||||
auto *property_lookup =
|
||||
storage.Create<PropertyLookup>(atom->identifier_, prop_pair.first);
|
||||
auto *prop_equal =
|
||||
storage.Create<EqualOperator>(property_lookup, prop_pair.second);
|
||||
collector.symbols_.insert(symbol); // PropertyLookup uses the symbol.
|
||||
all_filters_.emplace_back(prop_equal, collector.symbols_);
|
||||
}
|
||||
};
|
||||
auto add_node_filter = [&](NodeAtom *node) {
|
||||
const auto &node_symbol = symbol_table.at(*node->identifier_);
|
||||
if (!node->labels_.empty()) {
|
||||
// Store the filtered labels.
|
||||
label_filters_[node_symbol].insert(node->labels_.begin(),
|
||||
node->labels_.end());
|
||||
// Create a LabelsTest and store it in all_filters_.
|
||||
all_filters_.emplace_back(
|
||||
storage.Create<LabelsTest>(node->identifier_, node->labels_),
|
||||
std::unordered_set<Symbol>{node_symbol});
|
||||
}
|
||||
add_properties_filter(node);
|
||||
};
|
||||
auto add_expand_filter = [&](NodeAtom *prev_node, EdgeAtom *edge,
|
||||
NodeAtom *node) {
|
||||
const auto &edge_symbol = symbol_table.at(*edge->identifier_);
|
||||
if (!edge->edge_types_.empty()) {
|
||||
all_filters_.emplace_back(
|
||||
storage.Create<EdgeTypeTest>(edge->identifier_, edge->edge_types_),
|
||||
std::unordered_set<Symbol>{edge_symbol});
|
||||
}
|
||||
add_properties_filter(edge);
|
||||
add_node_filter(node);
|
||||
};
|
||||
ForeachPattern(pattern, add_node_filter, add_expand_filter);
|
||||
}
|
||||
|
||||
// Adds the where filter expression to all filters and collects additional
|
||||
// information for potential property and label indexing.
|
||||
void Filters::CollectWhereFilter(Where &where,
|
||||
const SymbolTable &symbol_table) {
|
||||
UsedSymbolsCollector collector(symbol_table);
|
||||
where.expression_->Accept(collector);
|
||||
all_filters_.emplace_back(where.expression_, collector.symbols_);
|
||||
AnalyzeFilter(where.expression_, symbol_table);
|
||||
}
|
||||
|
||||
// Converts a Query to multiple QueryParts. In the process new Ast nodes may be
|
||||
// created, e.g. filter expressions.
|
||||
std::vector<QueryPart> CollectQueryParts(const SymbolTable &symbol_table,
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
* Outputs a collection of items to the given stream, separating them with the
|
||||
@ -40,3 +41,22 @@ void PrintIterable(TStream &stream, const TIterable &iterable,
|
||||
PrintIterable(stream, iterable, delim,
|
||||
[](auto &stream, const auto &item) { stream << item; });
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a mapping if it exists for given key or returns the provided value.
|
||||
*
|
||||
* @param map A map-like container which has the `find` method.
|
||||
* @param key Key to look for in the map.
|
||||
* @param or_value Value to use if the key is not in the map.
|
||||
* @return Pair of value and a boolean indicating whether the key was in the
|
||||
* map.
|
||||
*/
|
||||
template <class TMap, class TKey, class TVal>
|
||||
std::pair<TVal, bool> FindOr(const TMap &map, const TKey &key,
|
||||
TVal &&or_value) {
|
||||
auto it = map.find(key);
|
||||
if (it != map.end()) {
|
||||
return {it->second, true};
|
||||
}
|
||||
return {std::forward<TVal>(or_value), false};
|
||||
}
|
||||
|
@ -50,6 +50,8 @@ class PlanChecker : public HierarchicalLogicalOperatorVisitor {
|
||||
PRE_VISIT(Delete);
|
||||
PRE_VISIT(ScanAll);
|
||||
PRE_VISIT(ScanAllByLabel);
|
||||
PRE_VISIT(ScanAllByLabelPropertyValue);
|
||||
PRE_VISIT(ScanAllByLabelPropertyRange);
|
||||
PRE_VISIT(Expand);
|
||||
PRE_VISIT(Filter);
|
||||
PRE_VISIT(Produce);
|
||||
@ -118,6 +120,8 @@ using ExpectCreateExpand = OpChecker<CreateExpand>;
|
||||
using ExpectDelete = OpChecker<Delete>;
|
||||
using ExpectScanAll = OpChecker<ScanAll>;
|
||||
using ExpectScanAllByLabel = OpChecker<ScanAllByLabel>;
|
||||
using ExpectScanAllByLabelPropertyRange =
|
||||
OpChecker<ScanAllByLabelPropertyRange>;
|
||||
using ExpectExpand = OpChecker<Expand>;
|
||||
using ExpectFilter = OpChecker<Filter>;
|
||||
using ExpectProduce = OpChecker<Produce>;
|
||||
@ -208,6 +212,27 @@ class ExpectOptional : public OpChecker<Optional> {
|
||||
const std::list<BaseOpChecker *> &optional_;
|
||||
};
|
||||
|
||||
class ExpectScanAllByLabelPropertyValue
|
||||
: public OpChecker<ScanAllByLabelPropertyValue> {
|
||||
public:
|
||||
ExpectScanAllByLabelPropertyValue(GraphDbTypes::Label label,
|
||||
GraphDbTypes::Property property,
|
||||
query::Expression *expression)
|
||||
: label_(label), property_(property), expression_(expression) {}
|
||||
|
||||
void ExpectOp(ScanAllByLabelPropertyValue &scan_all,
|
||||
const SymbolTable &) override {
|
||||
EXPECT_EQ(scan_all.label(), label_);
|
||||
EXPECT_EQ(scan_all.property(), property_);
|
||||
EXPECT_EQ(scan_all.expression(), expression_);
|
||||
}
|
||||
|
||||
private:
|
||||
GraphDbTypes::Label label_;
|
||||
GraphDbTypes::Property property_;
|
||||
query::Expression *expression_;
|
||||
};
|
||||
|
||||
class ExpectCreateIndex : public OpChecker<CreateIndex> {
|
||||
public:
|
||||
ExpectCreateIndex(GraphDbTypes::Label label, GraphDbTypes::Property property)
|
||||
@ -936,4 +961,142 @@ TEST(TestLogicalPlanner, CreateIndex) {
|
||||
CheckPlan(storage, ExpectCreateIndex(label, property));
|
||||
}
|
||||
|
||||
TEST(TestLogicalPlanner, AtomIndexedLabelProperty) {
|
||||
// Test MATCH (n :label {property: 42, not_indexed: 0}) RETURN n
|
||||
AstTreeStorage storage;
|
||||
Dbms dbms;
|
||||
auto dba = dbms.active();
|
||||
auto label = dba->label("label");
|
||||
auto property = dba->property("property");
|
||||
auto not_indexed = dba->property("not_indexed");
|
||||
auto vertex = dba->insert_vertex();
|
||||
vertex.add_label(label);
|
||||
vertex.PropsSet(property, 42);
|
||||
dba->commit();
|
||||
dba = dbms.active();
|
||||
dba->BuildIndex(label, property);
|
||||
dba = dbms.active();
|
||||
auto node = NODE("n", label);
|
||||
auto lit_42 = LITERAL(42);
|
||||
node->properties_[property] = lit_42;
|
||||
node->properties_[not_indexed] = LITERAL(0);
|
||||
QUERY(MATCH(PATTERN(node)), RETURN("n"));
|
||||
auto symbol_table = MakeSymbolTable(*storage.query());
|
||||
auto plan =
|
||||
MakeLogicalPlan<RuleBasedPlanner>(storage, symbol_table, dba.get());
|
||||
CheckPlan(*plan, symbol_table,
|
||||
ExpectScanAllByLabelPropertyValue(label, property, lit_42),
|
||||
ExpectFilter(), ExpectProduce());
|
||||
}
|
||||
|
||||
TEST(TestLogicalPlanner, AtomPropertyWhereLabelIndexing) {
|
||||
// Test MATCH (n {property: 42}) WHERE n.not_indexed AND n:label RETURN n
|
||||
AstTreeStorage storage;
|
||||
Dbms dbms;
|
||||
auto dba = dbms.active();
|
||||
auto label = dba->label("label");
|
||||
auto property = dba->property("property");
|
||||
auto not_indexed = dba->property("not_indexed");
|
||||
dba->BuildIndex(label, property);
|
||||
dba = dbms.active();
|
||||
auto node = NODE("n");
|
||||
auto lit_42 = LITERAL(42);
|
||||
node->properties_[property] = lit_42;
|
||||
QUERY(MATCH(PATTERN(node)),
|
||||
WHERE(AND(PROPERTY_LOOKUP("n", not_indexed),
|
||||
storage.Create<query::LabelsTest>(
|
||||
IDENT("n"), std::vector<GraphDbTypes::Label>{label}))),
|
||||
RETURN("n"));
|
||||
auto symbol_table = MakeSymbolTable(*storage.query());
|
||||
auto plan =
|
||||
MakeLogicalPlan<RuleBasedPlanner>(storage, symbol_table, dba.get());
|
||||
CheckPlan(*plan, symbol_table,
|
||||
ExpectScanAllByLabelPropertyValue(label, property, lit_42),
|
||||
ExpectFilter(), ExpectProduce());
|
||||
}
|
||||
|
||||
TEST(TestLogicalPlanner, WhereIndexedLabelProperty) {
|
||||
// Test MATCH (n :label) WHERE n.property = 42 RETURN n
|
||||
AstTreeStorage storage;
|
||||
Dbms dbms;
|
||||
auto dba = dbms.active();
|
||||
auto label = dba->label("label");
|
||||
auto property = dba->property("property");
|
||||
dba->BuildIndex(label, property);
|
||||
dba = dbms.active();
|
||||
auto lit_42 = LITERAL(42);
|
||||
QUERY(MATCH(PATTERN(NODE("n", label))),
|
||||
WHERE(EQ(PROPERTY_LOOKUP("n", property), lit_42)), RETURN("n"));
|
||||
auto symbol_table = MakeSymbolTable(*storage.query());
|
||||
auto plan =
|
||||
MakeLogicalPlan<RuleBasedPlanner>(storage, symbol_table, dba.get());
|
||||
CheckPlan(*plan, symbol_table,
|
||||
ExpectScanAllByLabelPropertyValue(label, property, lit_42),
|
||||
ExpectFilter(), ExpectProduce());
|
||||
}
|
||||
|
||||
TEST(TestLogicalPlanner, BestPropertyIndexed) {
|
||||
// Test MATCH (n :label) WHERE n.property = 1 AND n.better = 42 RETURN n
|
||||
AstTreeStorage storage;
|
||||
Dbms dbms;
|
||||
auto dba = dbms.active();
|
||||
auto label = dba->label("label");
|
||||
auto property = dba->property("property");
|
||||
dba->BuildIndex(label, property);
|
||||
dba = dbms.active();
|
||||
// Add a vertex with :label+property combination, so that the best
|
||||
// :label+better remains empty and thus better choice.
|
||||
auto vertex = dba->insert_vertex();
|
||||
vertex.add_label(label);
|
||||
vertex.PropsSet(property, 1);
|
||||
dba->commit();
|
||||
dba = dbms.active();
|
||||
ASSERT_EQ(dba->vertices_count(label, property), 1);
|
||||
auto better = dba->property("better");
|
||||
dba->BuildIndex(label, better);
|
||||
dba = dbms.active();
|
||||
ASSERT_EQ(dba->vertices_count(label, better), 0);
|
||||
auto lit_42 = LITERAL(42);
|
||||
QUERY(MATCH(PATTERN(NODE("n", label))),
|
||||
WHERE(AND(EQ(PROPERTY_LOOKUP("n", property), LITERAL(1)),
|
||||
EQ(PROPERTY_LOOKUP("n", better), lit_42))),
|
||||
RETURN("n"));
|
||||
auto symbol_table = MakeSymbolTable(*storage.query());
|
||||
auto plan =
|
||||
MakeLogicalPlan<RuleBasedPlanner>(storage, symbol_table, dba.get());
|
||||
CheckPlan(*plan, symbol_table,
|
||||
ExpectScanAllByLabelPropertyValue(label, better, lit_42),
|
||||
ExpectFilter(), ExpectProduce());
|
||||
}
|
||||
|
||||
TEST(TestLogicalPlanner, MultiPropertyIndexScan) {
|
||||
// Test MATCH (n :label1), (m :label2) WHERE n.prop1 = 1 AND m.prop2 = 2
|
||||
// RETURN n, m
|
||||
Dbms dbms;
|
||||
auto dba = dbms.active();
|
||||
auto label1 = dba->label("label1");
|
||||
auto label2 = dba->label("label2");
|
||||
auto prop1 = dba->property("prop1");
|
||||
auto prop2 = dba->property("prop2");
|
||||
dba->BuildIndex(label1, prop1);
|
||||
dba = dbms.active();
|
||||
dba->BuildIndex(label2, prop2);
|
||||
dba = dbms.active();
|
||||
AstTreeStorage storage;
|
||||
auto lit_1 = LITERAL(1);
|
||||
auto lit_2 = LITERAL(2);
|
||||
QUERY(MATCH(PATTERN(NODE("n", label1)), PATTERN(NODE("m", label2))),
|
||||
WHERE(AND(EQ(PROPERTY_LOOKUP("n", prop1), lit_1),
|
||||
EQ(PROPERTY_LOOKUP("m", prop2), lit_2))),
|
||||
RETURN("n", "m"));
|
||||
auto symbol_table = MakeSymbolTable(*storage.query());
|
||||
auto plan =
|
||||
MakeLogicalPlan<RuleBasedPlanner>(storage, symbol_table, dba.get());
|
||||
CheckPlan(*plan, symbol_table,
|
||||
ExpectScanAllByLabelPropertyValue(label1, prop1, lit_1),
|
||||
ExpectFilter(),
|
||||
ExpectScanAllByLabelPropertyValue(label2, prop2, lit_2),
|
||||
ExpectFilter(), ExpectProduce());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
@ -52,7 +52,7 @@ class PrintOperatorTree(gdb.Command):
|
||||
next_op = operator.cast(operator.dynamic_type)
|
||||
tree = []
|
||||
while next_op is not None:
|
||||
tree.append('* %s' % next_op.type.name)
|
||||
tree.append('* %s <%s>' % (next_op.type.name, next_op.address))
|
||||
next_op = _get_operator_input(next_op)
|
||||
print('\n'.join(tree))
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user