diff --git a/src/engine/QueryPlanner.h b/src/engine/QueryPlanner.h index 94c6ce9a30..88d2ca39af 100644 --- a/src/engine/QueryPlanner.h +++ b/src/engine/QueryPlanner.h @@ -65,15 +65,8 @@ class QueryPlanner { Node(size_t id, SparqlTriple t, std::optional graphVariable = std::nullopt) : id_(id), triple_(std::move(t)) { - if (triple_.s_.isVariable()) { - _variables.insert(triple_.s_.getVariable()); - } - if (auto predicate = triple_.getPredicateVariable()) { - _variables.insert(predicate.value()); - } - if (triple_.o_.isVariable()) { - _variables.insert(triple_.o_.getVariable()); - } + triple_.forEachVariable( + [this](const auto& var) { _variables.insert(var); }); if (graphVariable.has_value()) { _variables.insert(std::move(graphVariable).value()); } diff --git a/src/parser/CMakeLists.txt b/src/parser/CMakeLists.txt index 8c76f41716..1629ea784d 100644 --- a/src/parser/CMakeLists.txt +++ b/src/parser/CMakeLists.txt @@ -30,5 +30,6 @@ add_library(parser Quads.cpp UpdateTriples.cpp MaterializedViewQuery.cpp + GraphPatternAnalysis.cpp ) qlever_target_link_libraries(parser sparqlParser parserData sparqlExpressions rdfEscaping global re2::re2 util engine index rdfTypes) diff --git a/src/parser/GraphPatternAnalysis.cpp b/src/parser/GraphPatternAnalysis.cpp new file mode 100644 index 0000000000..8969420ae4 --- /dev/null +++ b/src/parser/GraphPatternAnalysis.cpp @@ -0,0 +1,30 @@ +// Copyright 2026 The QLever Authors, in particular: +// +// 2026 Christoph Ullinger , UFR +// +// UFR = University of Freiburg, Chair of Algorithms and Data Structures + +#include "parser/GraphPatternAnalysis.h" + +namespace graphPatternAnalysis { + +// _____________________________________________________________________________ +bool BasicGraphPatternsInvariantTo::operator()( + const parsedQuery::Bind& bind) const { + return !variables_.contains(bind._target); +} + +// _____________________________________________________________________________ +bool BasicGraphPatternsInvariantTo::operator()( + const parsedQuery::Values& valuesClause) const { + const auto& [variables, values] = valuesClause._inlineValues; + return + // There is exactly one row inside the `VALUES`. + values.size() == 1 && + // The `VALUES` doesn't bind to any of the `variables_`. + ql::ranges::none_of(variables, [this](const auto& var) { + return variables_.contains(var); + }); +} + +} // namespace graphPatternAnalysis diff --git a/src/parser/GraphPatternAnalysis.h b/src/parser/GraphPatternAnalysis.h new file mode 100644 index 0000000000..91f7c659c0 --- /dev/null +++ b/src/parser/GraphPatternAnalysis.h @@ -0,0 +1,53 @@ +// Copyright 2026 The QLever Authors, in particular: +// +// 2026 Christoph Ullinger , UFR +// +// UFR = University of Freiburg, Chair of Algorithms and Data Structures + +#ifndef QLEVER_SRC_PARSER_GRAPHPATTERNANALYSIS_H_ +#define QLEVER_SRC_PARSER_GRAPHPATTERNANALYSIS_H_ + +#include "parser/GraphPatternOperation.h" + +// This module contains helpers for analyzing the structure of graph patterns. + +// _____________________________________________________________________________ +namespace graphPatternAnalysis { + +// Check whether certain graph patterns can be ignored when we are only +// interested in the bindings for variables from `variables_` as they do not +// affect the result for these `variables_`. +// +// For example: A basic graph pattern (a list of triples) is invariant to a +// `BIND` statement whose target variable is not contained in the basic graph +// pattern, because the `BIND` only adds its own column, but neither adds nor +// deletes result rows. +// +// This is currently used for the `MaterializedViewsManager`'s +// `QueryPatternCache`. +// +// NOTE: This does not guarantee completeness, so it might return `false` even +// though we could be invariant to a `GraphPatternOperation`. +struct BasicGraphPatternsInvariantTo { + ad_utility::HashSet variables_; + + bool operator()(const parsedQuery::Bind& bind) const; + bool operator()(const parsedQuery::Values& values) const; + + template + bool operator()(const T&) const { + // The presence of any of these operations might remove or duplicate rows. + namespace pq = parsedQuery; + static_assert( + ad_utility::SimilarToAny< + T, pq::Optional, pq::Union, pq::Subquery, pq::TransPath, + pq::BasicGraphPattern, pq::Service, pq::PathQuery, pq::SpatialQuery, + pq::TextSearchQuery, pq::Minus, pq::GroupGraphPattern, pq::Describe, + pq::Load, pq::NamedCachedResult, pq::MaterializedViewQuery>); + return false; + } +}; + +} // namespace graphPatternAnalysis + +#endif // QLEVER_SRC_PARSER_GRAPHPATTERNANALYSIS_H_ diff --git a/src/parser/GraphPatternOperation.cpp b/src/parser/GraphPatternOperation.cpp index b92e2eb46d..027adbbaaa 100644 --- a/src/parser/GraphPatternOperation.cpp +++ b/src/parser/GraphPatternOperation.cpp @@ -16,6 +16,7 @@ #include "parser/TripleComponent.h" #include "util/Exception.h" #include "util/Forward.h" +#include "util/VariantRangeFilter.h" namespace parsedQuery { @@ -81,4 +82,26 @@ void BasicGraphPattern::appendTriples(BasicGraphPattern other) { auto inner = _expression.getDescriptor(); return "BIND (" + inner + " AS " + _target.name() + ")"; } + +// ____________________________________________________________________________ +void BasicGraphPattern::collectAllContainedVariables( + ad_utility::HashSet& vars) const { + for (const SparqlTriple& t : _triples) { + t.forEachVariable([&vars](const auto& var) { vars.insert(var); }); + } +} + +// _____________________________________________________________________________ +ad_utility::HashSet getVariablesPresentInFirstBasicGraphPattern( + const std::vector& graphPatterns) { + ad_utility::HashSet vars; + auto basicGraphPatterns = + ad_utility::filterRangeOfVariantsByType( + graphPatterns); + if (!ql::ranges::empty(basicGraphPatterns)) { + (*basicGraphPatterns.begin()).collectAllContainedVariables(vars); + } + return vars; +} + } // namespace parsedQuery diff --git a/src/parser/GraphPatternOperation.h b/src/parser/GraphPatternOperation.h index 4f16be3f9a..9e6687d58a 100644 --- a/src/parser/GraphPatternOperation.h +++ b/src/parser/GraphPatternOperation.h @@ -80,8 +80,21 @@ struct BasicGraphPattern { std::vector _triples; /// Append the triples from `other` to this `BasicGraphPattern` void appendTriples(BasicGraphPattern other); + + // Collect all the `Variable`s present in this `BasicGraphPattern` and add + // them to a `HashSet`. + void collectAllContainedVariables(ad_utility::HashSet& vars) const; }; +// Extract all variables present in a the first `BasicGraphPattern` contained in +// a vector of `GraphPatternOperation`s. It is used for skipping some graph +// patterns in `MaterializedViewQueryAnalysis.cpp`. +// +// IMPORTANT: This function does not consider variables that are contained in +// other types of `GraphPatternOperation`s. +ad_utility::HashSet getVariablesPresentInFirstBasicGraphPattern( + const std::vector& graphPatterns); + /// A `Values` clause struct Values { SparqlValues _inlineValues; diff --git a/src/parser/MaterializedViewQuery.cpp b/src/parser/MaterializedViewQuery.cpp index 7ad56493e9..2ee31d2a4d 100644 --- a/src/parser/MaterializedViewQuery.cpp +++ b/src/parser/MaterializedViewQuery.cpp @@ -92,6 +92,12 @@ MaterializedViewQuery::MaterializedViewQuery(const SparqlTriple& triple) { addRequestedColumn(requestedColumn, simpleTriple.o_); } +// _____________________________________________________________________________ +MaterializedViewQuery::MaterializedViewQuery(std::string name, + RequestedColumns requestedColumns) + : viewName_{std::move(name)}, + requestedColumns_{std::move(requestedColumns)} {}; + // _____________________________________________________________________________ ad_utility::HashSet MaterializedViewQuery::getVarsToKeep() const { ad_utility::HashSet varsToKeep; diff --git a/src/parser/MaterializedViewQuery.h b/src/parser/MaterializedViewQuery.h index 4a44b4f9fc..886e239943 100644 --- a/src/parser/MaterializedViewQuery.h +++ b/src/parser/MaterializedViewQuery.h @@ -47,7 +47,8 @@ struct MaterializedViewQuery : MagicServiceQuery { // column names in the query result or literals/IRIs to restrict the column // on. This can be used for filtering the results and reading any number of // payload columns from the materialized view. - ad_utility::HashMap requestedColumns_; + using RequestedColumns = ad_utility::HashMap; + RequestedColumns requestedColumns_; // This constructor takes an IRI consisting of the magic service IRI for // materialized views with the view name as a suffix. If this is used, add the @@ -58,6 +59,9 @@ struct MaterializedViewQuery : MagicServiceQuery { // are necessary in this case. explicit MaterializedViewQuery(const SparqlTriple& triple); + // For query rewriting: Initialize directly using name and requested columns. + MaterializedViewQuery(std::string name, RequestedColumns requestedColumns); + void addParameter(const SparqlTriple& triple) override; // Return the variables that should be visible from this read on the diff --git a/src/parser/PropertyPath.cpp b/src/parser/PropertyPath.cpp index cccfad61fe..b405a70ccc 100644 --- a/src/parser/PropertyPath.cpp +++ b/src/parser/PropertyPath.cpp @@ -130,6 +130,18 @@ bool PropertyPath::isIri() const { return std::holds_alternative(path_); } +// _____________________________________________________________________________ +const std::vector& PropertyPath::getSequence() const { + AD_CONTRACT_CHECK(isSequence()); + return std::get(path_).children_; +} + +// _____________________________________________________________________________ +bool PropertyPath::isSequence() const { + return std::holds_alternative(path_) && + std::get(path_).modifier_ == Modifier::SEQUENCE; +} + // _____________________________________________________________________________ std::optional> PropertyPath::getChildOfInvertedPath() const { diff --git a/src/parser/PropertyPath.h b/src/parser/PropertyPath.h index 08fa199863..0daab58efe 100644 --- a/src/parser/PropertyPath.h +++ b/src/parser/PropertyPath.h @@ -134,6 +134,13 @@ class PropertyPath { // otherwise. bool isIri() const; + // If the path is a sequence, return the children (that is, the parts of the + // sequence). If the path is not a sequence this will throw. + const std::vector& getSequence() const; + + // Check if the path is a sequence. + bool isSequence() const; + // If the path is a modified path with an inverse modifier, return the pointer // to its only child. Otherwise, return nullptr. std::optional> diff --git a/src/parser/SparqlTriple.h b/src/parser/SparqlTriple.h index 2ba815c743..e0212a4a73 100644 --- a/src/parser/SparqlTriple.h +++ b/src/parser/SparqlTriple.h @@ -133,6 +133,19 @@ class SparqlTriple auto ptr = std::get_if(&p_); return (ptr != nullptr && *ptr == variable); } + + // Call a function for every variable contained in the triple. + void forEachVariable(auto function) const { + if (s_.isVariable()) { + function(s_.getVariable()); + } + if (auto predicate = getPredicateVariable()) { + function(predicate.value()); + } + if (o_.isVariable()) { + function(o_.getVariable()); + } + } }; #endif // QLEVER_SRC_PARSER_SPARQLTRIPLE_H diff --git a/src/util/StringPairHashMap.h b/src/util/StringPairHashMap.h new file mode 100644 index 0000000000..26a0083cd0 --- /dev/null +++ b/src/util/StringPairHashMap.h @@ -0,0 +1,71 @@ +// Copyright 2026 The QLever Authors, in particular: +// +// 2026 Christoph Ullinger , UFR +// +// UFR = University of Freiburg, Chair of Algorithms and Data Structures + +#ifndef QLEVER_SRC_UTIL_STRINGPAIRHASHMAP_H_ +#define QLEVER_SRC_UTIL_STRINGPAIRHASHMAP_H_ + +#include "util/HashMap.h" + +// This module provides a modified version of `ad_utility::HashMap` that uses +// pairs of strings as keys. Unlike the default hash map it allows looking up +// values with pairs of string views as keys. This is implemented using custom +// hash and equality operators. + +// TODO This could be extended to support `std::tuple` or +// `std::array`, not only `std::pair`, and other transparently comparable types. + +// _____________________________________________________________________________ +namespace ad_utility { + +// _____________________________________________________________________________ +namespace detail { + +using StringPair = std::pair; +using StringViewPair = std::pair; + +// _____________________________________________________________________________ +struct StringPairHash { + // Allows looking up values from a hash map with `StringPair` keys also with + // `StringViewPair`. + using is_transparent = void; + + size_t operator()(const StringPair& p) const { + return absl::HashOf(p.first, p.second); + } + + size_t operator()(const StringViewPair& p) const { + return absl::HashOf(p.first, p.second); + } +}; + +// _____________________________________________________________________________ +struct StringPairEq { + using is_transparent = void; + + bool operator()(const StringPair& a, const StringPair& b) const { + return a == b; + } + + bool operator()(const StringPair& a, const StringViewPair& b) const { + return a.first == b.first && a.second == b.second; + } + + bool operator()(const StringViewPair& a, const StringPair& b) const { + return b.first == a.first && b.second == a.second; + } +}; + +} // namespace detail + +template +using StringPairHashMap = + ad_utility::HashMap; + +} // namespace ad_utility + +#endif // QLEVER_SRC_UTIL_STRINGPAIRHASHMAP_H_ diff --git a/src/util/VariantRangeFilter.h b/src/util/VariantRangeFilter.h new file mode 100644 index 0000000000..b6a5661488 --- /dev/null +++ b/src/util/VariantRangeFilter.h @@ -0,0 +1,26 @@ +// Copyright 2026 The QLever Authors, in particular: +// +// 2026 Christoph Ullinger , UFR +// +// UFR = University of Freiburg, Chair of Algorithms and Data Structures + +#ifndef QLEVER_SRC_UTIL_VARIANTRANGEFILTER_H +#define QLEVER_SRC_UTIL_VARIANTRANGEFILTER_H + +#include "backports/algorithm.h" +#include "util/TransparentFunctors.h" + +namespace ad_utility { + +// Helper that filters a range, like `std::vector` which contains `std::variant` +// elements by a certain type `T` and returns a view of the contained values. +CPP_template(typename T, typename R)( + requires ql::ranges::range) auto filterRangeOfVariantsByType(const R& + range) { + return range | ql::views::filter(holdsAlternative) | + ql::views::transform(get); +} + +} // namespace ad_utility + +#endif // QLEVER_SRC_UTIL_VARIANTRANGEFILTER_H diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 32aa042017..6653abc2ea 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -193,6 +193,8 @@ addLinkAndDiscoverTestSerial(QueryPlannerSpatialJoinTest engine) addLinkAndDiscoverTestNoLibs(HashMapTest) +addLinkAndDiscoverTestNoLibs(StringPairHashMapTest) + addLinkAndDiscoverTest(HashSetTest) addLinkAndDiscoverTestSerial(GroupByTest engine) @@ -493,3 +495,5 @@ addLinkAndDiscoverTest(MaterializedViewsTest qlever engine server) addLinkAndDiscoverTestNoLibs(ConstexprMapTest) addLinkAndDiscoverTestNoLibs(ParallelExecutorTest) + +addLinkAndDiscoverTestNoLibs(VariantRangeFilterTest) diff --git a/test/MaterializedViewsTest.cpp b/test/MaterializedViewsTest.cpp index 887263df63..6331e0c6b2 100644 --- a/test/MaterializedViewsTest.cpp +++ b/test/MaterializedViewsTest.cpp @@ -556,6 +556,16 @@ TEST_F(MaterializedViewsTest, ManualConfigurations) { ::testing::Eq(V{"?o"}))); } + // Test internal constructor. + { + ViewQuery query{"testView", ViewQuery::RequestedColumns{ + {V{"?s"}, V{"?s2"}}, {V{"?o"}, V{"?o2"}}}}; + EXPECT_EQ(query.viewName_, "testView"); + EXPECT_THAT(query.getVarsToKeep(), + ::testing::UnorderedElementsAre(::testing::Eq(V{"?s2"}), + ::testing::Eq(V{"?o2"}))); + } + // Unsupported format version. { auto plan = qlv().parseAndPlanQuery(simpleWriteQuery_); diff --git a/test/StringPairHashMapTest.cpp b/test/StringPairHashMapTest.cpp new file mode 100644 index 0000000000..d6ee1b3dfd --- /dev/null +++ b/test/StringPairHashMapTest.cpp @@ -0,0 +1,62 @@ +// Copyright 2026 The QLever Authors, in particular: +// +// 2026 Christoph Ullinger , UFR +// +// UFR = University of Freiburg, Chair of Algorithms and Data Structures + +#include + +#include "util/StringPairHashMap.h" + +// _____________________________________________________________________________ +TEST(StringPairHashMapTest, InsertAndLookup) { + ad_utility::StringPairHashMap map; + + using ad_utility::detail::StringPair; + using ad_utility::detail::StringViewPair; + + // Insert using `std::string` pairs. + map[StringPair{"hello", "world"}] = 7; + map[StringPair{"foo", "bar"}] = 42; + + ASSERT_EQ(map.size(), 2u); + + // Lookup using `std::string_view` pairs. + auto it = map.find(StringViewPair{"hello", "world"}); + ASSERT_NE(it, map.end()); + ASSERT_EQ(it->second, 7); + + EXPECT_EQ(map.count(StringViewPair{"foo", "bar"}), 1u); + EXPECT_EQ(map.count(StringViewPair{"does not", "exist"}), 0u); +} + +// _____________________________________________________________________________ +TEST(StringPairHashMapTest, StringPairEq) { + using ad_utility::detail::StringPair; + using ad_utility::detail::StringViewPair; + ad_utility::detail::StringPairEq eq; + + StringPair a{"a", "b"}; + StringPair b{"x", "y"}; + StringPair c{"x", "g"}; + + EXPECT_TRUE(eq(a, a)); + EXPECT_FALSE(eq(a, b)); + EXPECT_FALSE(eq(a, c)); + + StringViewPair aEq{"a", "b"}; + StringViewPair aNe{"a", "c"}; + StringViewPair bNe{"f", "g"}; + + EXPECT_TRUE(eq(a, aEq)); + EXPECT_FALSE(eq(a, aNe)); + EXPECT_FALSE(eq(b, bNe)); + + EXPECT_TRUE(eq(aEq, a)); + EXPECT_FALSE(eq(aNe, a)); + EXPECT_FALSE(eq(bNe, b)); + + StringViewPair aSv{"a", "b"}; + + EXPECT_TRUE(eq(a, aSv)); +} diff --git a/test/VariantRangeFilterTest.cpp b/test/VariantRangeFilterTest.cpp new file mode 100644 index 0000000000..ca9036fd56 --- /dev/null +++ b/test/VariantRangeFilterTest.cpp @@ -0,0 +1,38 @@ +// Copyright 2026 The QLever Authors, in particular: +// +// 2026 Christoph Ullinger , UFR +// +// UFR = University of Freiburg, Chair of Algorithms and Data Structures + +#include + +#include "./util/GTestHelpers.h" +#include "util/VariantRangeFilter.h" + +namespace { + +// Helper for testing `filterRangeOfVariantsByType`. +template +void expectFilteredRange( + std::vector input, std::vector expected, + ad_utility::source_location location = AD_CURRENT_SOURCE_LOC()) { + auto l = generateLocationTrace(location); + auto matcher = liftMatcherToElementsAreArray>( + [](auto value) { return ::testing::Eq(value); }); + auto actual = ad_utility::filterRangeOfVariantsByType(input) | + ::ranges::to>; + EXPECT_THAT(actual, matcher(expected)); +} + +// _____________________________________________________________________________ +TEST(VariantRangeFilterTest, Test) { + using V = std::variant; + std::vector vec{1, 'c', true, false, true, 3, 'f'}; + + expectFilteredRange(vec, {1, 3}); + expectFilteredRange(vec, {'c', 'f'}); + expectFilteredRange(vec, {true, false, true}); + expectFilteredRange(vec, {}); +} + +} // namespace diff --git a/test/parser/CMakeLists.txt b/test/parser/CMakeLists.txt index 8810d5f332..45cfc320e7 100644 --- a/test/parser/CMakeLists.txt +++ b/test/parser/CMakeLists.txt @@ -8,6 +8,7 @@ addLinkAndDiscoverTest(BlankNodeExpressionTest engine) addLinkAndDiscoverTest(PropertyPathTest parser) addLinkAndDiscoverTest(UpdateTriplesTest parser) addLinkAndDiscoverTest(NamedCachedResultTest parser) +addLinkAndDiscoverTest(GraphPatternOperationTest parser) addLinkAndDiscoverTestSerial(SparqlAntlrParserTest parser engine) addLinkAndDiscoverTestSerial(SparqlAntlrParserUpdateTest parser engine) addLinkAndDiscoverTestSerial(SparqlAntlrParserExpressionTest parser sparqlExpressions engine) diff --git a/test/parser/GraphPatternOperationTest.cpp b/test/parser/GraphPatternOperationTest.cpp new file mode 100644 index 0000000000..71b11e9aae --- /dev/null +++ b/test/parser/GraphPatternOperationTest.cpp @@ -0,0 +1,38 @@ +// Copyright 2026 The QLever Authors, in particular: +// +// 2026 Christoph Ullinger , UFR +// +// UFR = University of Freiburg, Chair of Algorithms and Data Structures + +#include + +#include "parser/GraphPatternOperation.h" +#include "parser/SparqlTriple.h" + +// _____________________________________________________________________________ +TEST(GraphPatternOperationTest, BasicPatternContainedVars) { + SparqlTripleSimple example1{Variable{"?s"}, Variable{"?p"}, Variable{"?o"}}; + SparqlTripleSimple example2{ + ad_utility::triple_component::Iri::fromIriref(""), + ad_utility::triple_component::Iri::fromIriref("

"), Variable{"?o2"}}; + + auto triple1 = SparqlTriple::fromSimple(example1); + auto triple2 = SparqlTriple::fromSimple(example2); + + parsedQuery::BasicGraphPattern bgp{{triple1, triple2}}; + + ad_utility::HashSet vars; + bgp.collectAllContainedVariables(vars); + auto expectedVarsMatcher = ::testing::UnorderedElementsAre( + ::testing::Eq(Variable{"?s"}), ::testing::Eq(Variable{"?p"}), + ::testing::Eq(Variable{"?o"}), ::testing::Eq(Variable{"?o2"})); + EXPECT_THAT(vars, expectedVarsMatcher); + + parsedQuery::Bind bind{ + sparqlExpression::SparqlExpressionPimpl::makeVariableExpression( + Variable{"?x"}), + Variable{"?y"}}; + std::vector graphPatterns{bind, bgp}; + EXPECT_THAT(getVariablesPresentInFirstBasicGraphPattern(graphPatterns), + expectedVarsMatcher); +} diff --git a/test/parser/PropertyPathTest.cpp b/test/parser/PropertyPathTest.cpp index 0d99b1791b..de2cd749e1 100644 --- a/test/parser/PropertyPathTest.cpp +++ b/test/parser/PropertyPathTest.cpp @@ -288,3 +288,33 @@ TEST(PropertyPath, handlePath) { }), 2); } + +// _____________________________________________________________________________ +TEST(PropertyPath, Getters) { + auto path1 = PropertyPath::fromIri(iri1); + EXPECT_TRUE(path1.isIri()); + EXPECT_FALSE(path1.isSequence()); + EXPECT_EQ(path1.getIri(), iri1); + + auto path2 = PropertyPath::makeInverse(PropertyPath::fromIri(iri1)); + EXPECT_FALSE(path2.isIri()); + EXPECT_FALSE(path2.isSequence()); + + auto path3 = PropertyPath::makeAlternative( + {PropertyPath::fromIri(iri1), PropertyPath::fromIri(iri2)}); + EXPECT_FALSE(path3.isIri()); + EXPECT_FALSE(path3.isSequence()); + + auto path4 = PropertyPath::makeSequence( + {PropertyPath::fromIri(iri1), PropertyPath::fromIri(iri2)}); + EXPECT_FALSE(path4.isIri()); + EXPECT_TRUE(path4.isSequence()); + auto matchIri = [](ad_utility::triple_component::Iri iri) + -> ::testing::Matcher { + return ::testing::AllOf( + ::testing::Property(&PropertyPath::isIri, ::testing::IsTrue()), + ::testing::Property(&PropertyPath::getIri, ::testing::Eq(iri))); + }; + EXPECT_THAT(path4.getSequence(), + ::testing::ElementsAre(matchIri(iri1), matchIri(iri2))); +}