diff --git a/arangod/Aql/Executor.cpp b/arangod/Aql/Executor.cpp index 1ecd5a8425..524b32dc85 100644 --- a/arangod/Aql/Executor.cpp +++ b/arangod/Aql/Executor.cpp @@ -192,7 +192,7 @@ std::unordered_map const Executor::FunctionNames{ { "VALUES", Function("VALUES", "AQL_VALUES", "a|b", true, true, false, true, true, &Functions::Values) }, { "MERGE", Function("MERGE", "AQL_MERGE", "la|+", true, true, false, true, true, &Functions::Merge) }, { "MERGE_RECURSIVE", Function("MERGE_RECURSIVE", "AQL_MERGE_RECURSIVE", "a,a|+", true, true, false, true, true) }, - { "DOCUMENT", Function("DOCUMENT", "AQL_DOCUMENT", "h.|.", false, false, true, false, true, &Functions::Document) }, + { "DOCUMENT", Function("DOCUMENT", "AQL_DOCUMENT", "h.|.", false, false, true, false, true, &Functions::Document, NotInCluster) }, { "MATCHES", Function("MATCHES", "AQL_MATCHES", ".,l|b", true, true, false, true, true) }, { "UNSET", Function("UNSET", "AQL_UNSET", "a,sl|+", true, true, false, true, true, &Functions::Unset) }, { "KEEP", Function("KEEP", "AQL_KEEP", "a,sl|+", true, true, false, true, true, &Functions::Keep) }, @@ -218,7 +218,7 @@ std::unordered_map const Executor::FunctionNames{ { "GRAPH_TRAVERSAL", Function("GRAPH_TRAVERSAL", "AQL_GRAPH_TRAVERSAL", "s,als,s|a", false, false, true, false, false) }, { "TRAVERSAL_TREE", Function("TRAVERSAL_TREE", "AQL_TRAVERSAL_TREE", "h,h,s,s,s|a", false, false, true, false, false) }, { "GRAPH_TRAVERSAL_TREE", Function("GRAPH_TRAVERSAL_TREE", "AQL_GRAPH_TRAVERSAL_TREE", "s,als,s,s|a", false, false, true, false, false) }, - { "EDGES", Function("EDGES", "AQL_EDGES", "h,s,s|l,o", true, false, true, false, false) }, + { "EDGES", Function("EDGES", "AQL_EDGES", "h,s,s|l,o", true, false, true, false, false, &Functions::Edges, NotInCluster) }, { "GRAPH_EDGES", Function("GRAPH_EDGES", "AQL_GRAPH_EDGES", "s,als|a", false, false, true, false, false) }, { "GRAPH_VERTICES", Function("GRAPH_VERTICES", "AQL_GRAPH_VERTICES", "s,als|a", false, false, true, false, false) }, { "NEIGHBORS", Function("NEIGHBORS", "AQL_NEIGHBORS", "h,h,s,s|l,a", true, false, true, false, false, &Functions::Neighbors, NotInCluster) }, diff --git a/arangod/Aql/Functions.cpp b/arangod/Aql/Functions.cpp index 69c7d4d752..9b49e73bdb 100644 --- a/arangod/Aql/Functions.cpp +++ b/arangod/Aql/Functions.cpp @@ -1799,6 +1799,51 @@ static inline Json ExpandShapedJson (VocShaper* shaper, return json; } +//////////////////////////////////////////////////////////////////////////////// +/// @brief Reads a document by cid and key +/// Also lazy locks the collection. +/// Returns null if the document does not exist +//////////////////////////////////////////////////////////////////////////////// + +static Json readDocument (triagens::arango::AqlTransaction* trx, + CollectionNameResolver const* resolver, + TRI_voc_cid_t cid, + char const* key) { + auto collection = trx->trxCollection(cid); + + if (collection == nullptr) { + int res = TRI_AddCollectionTransaction(trx->getInternals(), + cid, + TRI_TRANSACTION_READ, + trx->nestingLevel(), + true, + true); + if (res != TRI_ERROR_NO_ERROR) { + THROW_ARANGO_EXCEPTION(res); + } + TRI_EnsureCollectionsTransaction(trx->getInternals()); + collection = trx->trxCollection(cid); + + if (collection == nullptr) { + THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_INTERNAL, "collection is a nullptr"); + } + } + + TRI_doc_mptr_copy_t mptr; + int res = trx->readSingle(collection, &mptr, key); + + if (res != TRI_ERROR_NO_ERROR) { + return Json(Json::Null); + } + + return ExpandShapedJson( + collection->_collection->_collection->getShaper(), + resolver, + cid, + &mptr + ); +} + //////////////////////////////////////////////////////////////////////////////// /// @brief Transforms VertexId to Json //////////////////////////////////////////////////////////////////////////////// @@ -2780,6 +2825,208 @@ AqlValue Functions::Document (triagens::aql::Query* query, // Id has invalid format return AqlValue(new Json(Json::Null)); } + +//////////////////////////////////////////////////////////////////////////////// +/// @brief function Edges +//////////////////////////////////////////////////////////////////////////////// + +AqlValue Functions::Edges (triagens::aql::Query* query, + triagens::arango::AqlTransaction* trx, + FunctionParameters const& parameters) { + size_t const n = parameters.size(); + + if (n < 3 || 5 < n) { + THROW_ARANGO_EXCEPTION_PARAMS(TRI_ERROR_QUERY_FUNCTION_ARGUMENT_NUMBER_MISMATCH, "EDGES", (int) 3, (int) 5); + } + + Json collectionJson = ExtractFunctionParameter(trx, parameters, 0, false); + if (! collectionJson.isString()) { + THROW_ARANGO_EXCEPTION(TRI_ERROR_INTERNAL); + } + std::string collectionName = triagens::basics::JsonHelper::getStringValue(collectionJson.json(), ""); + + TRI_transaction_collection_t* collection = nullptr; + TRI_voc_cid_t cid; + RegisterCollectionInTransaction(trx, collectionName, cid, collection); + if (collection->_collection->_type != TRI_COL_TYPE_EDGE) { + RegisterWarning(query, "EDGES", TRI_ERROR_ARANGO_COLLECTION_TYPE_INVALID); + return AqlValue(new Json(Json::Null)); + } + + Json vertexJson = ExtractFunctionParameter(trx, parameters, 1, false); + if (! vertexJson.isString()) { + // Invalid Start vertex + return AqlValue(new Json(Json::Null)); + } + + std::string vertexId = basics::JsonHelper::getStringValue(vertexJson.json(), ""); + std::vector parts = triagens::basics::StringUtils::split(vertexId, "/"); + if (parts.size() != 2) { + // Invalid Start vertex + return AqlValue(new Json(Json::Null)); + } + + + Json directionJson = ExtractFunctionParameter(trx, parameters, 2, false); + if (! directionJson.isString()) { + RegisterWarning(query, "EDGES", TRI_ERROR_QUERY_FUNCTION_ARGUMENT_TYPE_MISMATCH); + return AqlValue(new Json(Json::Null)); + } + std::string dirString = basics::JsonHelper::getStringValue(directionJson.json(), ""); + // transform String to lower case + std::transform(dirString.begin(), dirString.end(), dirString.begin(), ::tolower); + + TRI_edge_direction_e direction; + + if (dirString == "inbound") { + direction = TRI_EDGE_IN; + } + else if (dirString == "outbound") { + direction = TRI_EDGE_OUT; + } + else if (dirString == "any") { + direction = TRI_EDGE_ANY; + } + else { + RegisterWarning(query, "EDGES", TRI_ERROR_QUERY_FUNCTION_ARGUMENT_TYPE_MISMATCH); + return AqlValue(new Json(Json::Null)); + } + auto resolver = trx->resolver(); + + TRI_voc_cid_t startCid = resolver->getCollectionId(parts[0]); + if (startCid == 0) { + THROW_ARANGO_EXCEPTION(TRI_ERROR_ARANGO_COLLECTION_NOT_FOUND); + } + + char* key = const_cast(parts[1].c_str()); + std::vector edges = TRI_LookupEdgesDocumentCollection( + collection->_collection->_collection, + direction, + startCid, + key + ); + + size_t resultCount = edges.size(); + + auto shaper = collection->_collection->_collection->getShaper(); + if (n > 3) { + // We might have examples + Json exampleJson = ExtractFunctionParameter(trx, parameters, 3, false); + if (! exampleJson.isNull()) { + bool buildMatcher = false; + if (exampleJson.isArray()) { + size_t exampleCount = exampleJson.size(); + // We only support objects here so validate + for (size_t k = 0; k < exampleCount; ++k) { + if (! exampleJson.at(k).isObject()) { + RegisterWarning(query, "EDGES", TRI_ERROR_QUERY_FUNCTION_ARGUMENT_TYPE_MISMATCH); + if (TRI_DeleteArrayJson(TRI_UNKNOWN_MEM_ZONE, exampleJson.json(), k)) { + --k; + --exampleCount; + } + else { + // Should never occur. + // If it occurs in production it will simply fall through + // it can only retern more results than expected and not do any harm. + TRI_ASSERT(false); + } + } + } + if (exampleCount > 0) { + buildMatcher = true; + } + // else we do not filter at all + } + else if (exampleJson.isObject()) { + buildMatcher = true; + } + else { + RegisterWarning(query, "EDGES", TRI_ERROR_QUERY_FUNCTION_ARGUMENT_TYPE_MISMATCH); + } + if (buildMatcher) { + try { + triagens::arango::ExampleMatcher matcher(exampleJson.json(), shaper, resolver); + for (size_t i = 0; i < resultCount; ++i) { + if (! matcher.matches(cid, &edges[i])) { + edges.erase(edges.begin() + i); + --i; + --resultCount; + } + } + } + catch (triagens::basics::Exception& e) { + if (e.code() != TRI_RESULT_ELEMENT_NOT_FOUND) { + throw e; + } + // Illegal match, we cannot filter anything + edges.clear(); + resultCount = 0; + } + } + } + } + + bool includeVertices = false; + if (n == 5) { + // We have options + Json options = ExtractFunctionParameter(trx, parameters, 4, false); + if (options.isObject() && options.has("includeVertices")) { + includeVertices = triagens::basics::JsonHelper::getBooleanValue(options.json(), "includeVertices", false); + } + } + + if (includeVertices) { + Json result(Json::Array, resultCount); + + for (size_t i = 0; i < resultCount; ++i) { + Json resultPair(Json::Object, 2); + resultPair.set("edge", ExpandShapedJson( + shaper, + resolver, + cid, + &(edges[i]) + )); + char const* targetKey; + TRI_voc_cid_t targetCid; + + switch (direction) { + case TRI_EDGE_OUT: + targetKey = TRI_EXTRACT_MARKER_TO_KEY(&edges[i]); + targetCid = TRI_EXTRACT_MARKER_TO_CID(&edges[i]); + break; + case TRI_EDGE_IN: + targetKey = TRI_EXTRACT_MARKER_FROM_KEY(&edges[i]); + targetCid = TRI_EXTRACT_MARKER_FROM_CID(&edges[i]); + break; + case TRI_EDGE_ANY: + targetKey = TRI_EXTRACT_MARKER_TO_KEY(&edges[i]); + targetCid = TRI_EXTRACT_MARKER_TO_CID(&edges[i]); + if (targetCid == startCid && strcmp(targetKey, key) == 0) { + targetKey = TRI_EXTRACT_MARKER_FROM_KEY(&edges[i]); + targetCid = TRI_EXTRACT_MARKER_FROM_CID(&edges[i]); + }; + break; + } + + resultPair.set("vertex", readDocument(trx, resolver, targetCid, targetKey)); + result.add(resultPair); + } + return AqlValue(new Json(TRI_UNKNOWN_MEM_ZONE, result.steal())); + } + Json result(Json::Array, resultCount); + + for (size_t i = 0; i < resultCount; ++i) { + result.add(ExpandShapedJson( + shaper, + resolver, + cid, + &(edges[i]) + )); + } + + return AqlValue(new Json(TRI_UNKNOWN_MEM_ZONE, result.steal())); +} + // ----------------------------------------------------------------------------- // --SECTION-- END-OF-FILE // ----------------------------------------------------------------------------- diff --git a/arangod/Aql/Functions.h b/arangod/Aql/Functions.h index b4235aead8..fb5b7559da 100644 --- a/arangod/Aql/Functions.h +++ b/arangod/Aql/Functions.h @@ -118,6 +118,7 @@ namespace triagens { static AqlValue ParseIdentifier (triagens::aql::Query*, triagens::arango::AqlTransaction*, FunctionParameters const&); static AqlValue Minus (triagens::aql::Query*, triagens::arango::AqlTransaction*, FunctionParameters const&); static AqlValue Document (triagens::aql::Query*, triagens::arango::AqlTransaction*, FunctionParameters const&); + static AqlValue Edges (triagens::aql::Query*, triagens::arango::AqlTransaction*, FunctionParameters const&); }; } diff --git a/arangod/VocBase/ExampleMatcher.cpp b/arangod/VocBase/ExampleMatcher.cpp index af64ab94f8..729c462946 100644 --- a/arangod/VocBase/ExampleMatcher.cpp +++ b/arangod/VocBase/ExampleMatcher.cpp @@ -203,7 +203,6 @@ void ExampleMatcher::fillExampleDefinition (TRI_json_t const* example, } else { // no attribute path found. this means the result will be empty - ExampleMatcher::cleanup(); THROW_ARANGO_EXCEPTION(TRI_RESULT_ELEMENT_NOT_FOUND); } } @@ -213,7 +212,6 @@ void ExampleMatcher::fillExampleDefinition (TRI_json_t const* example, auto value = TRI_ShapedJsonJson(_shaper, jsonValue, false); if (value == nullptr) { - ExampleMatcher::cleanup(); THROW_ARANGO_EXCEPTION(TRI_RESULT_ELEMENT_NOT_FOUND); } @@ -313,12 +311,21 @@ ExampleMatcher::ExampleMatcher (TRI_json_t const* example, ExampleDefinition def; try { ExampleMatcher::fillExampleDefinition(TRI_LookupArrayJson(example, i), resolver, def); + definitions.emplace_back(move(def)); } - catch (...) { - ExampleMatcher::cleanup(); - throw; + catch (triagens::basics::Exception& e) { + if (e.code() != TRI_RESULT_ELEMENT_NOT_FOUND) { + ExampleMatcher::cleanup(); + throw; + } + // Result not found might happen. Ignore here because all other elemens + // might be matched. } - definitions.emplace_back(move(def)); + } + if (definitions.size() == 0) { + // None of the given examples could ever match. + // Throw result not found so client can short circuit. + THROW_ARANGO_EXCEPTION(TRI_RESULT_ELEMENT_NOT_FOUND); } } } diff --git a/js/server/tests/aql-graph.js b/js/server/tests/aql-graph.js index 11eafe7662..9d720c1cc6 100644 --- a/js/server/tests/aql-graph.js +++ b/js/server/tests/aql-graph.js @@ -67,7 +67,7 @@ function ahuacatlQueryEdgesTestSuite () { vertex.save({ _key: "v7", name: "v7" }); function makeEdge (from, to) { - edge.save("UnitTestsAhuacatlVertex/" + from, "UnitTestsAhuacatlVertex/" + to, { what: from + "->" + to }); + edge.save("UnitTestsAhuacatlVertex/" + from, "UnitTestsAhuacatlVertex/" + to, { _key: from + "" + to, what: from + "->" + to }); } makeEdge("v1", "v2"); @@ -323,7 +323,44 @@ function ahuacatlQueryEdgesTestSuite () { }); }, +//////////////////////////////////////////////////////////////////////////////// +/// @brief checks EDGES() +//////////////////////////////////////////////////////////////////////////////// + testEdgesFilterExample : function () { + var queries = [ + "FOR e IN EDGES(UnitTestsAhuacatlEdge, @start, 'any', @examples) SORT e.what RETURN e.what", + "FOR e IN NOOPT(EDGES(UnitTestsAhuacatlEdge, @start, 'any', @examples)) SORT e.what RETURN e.what", + "FOR e IN NOOPT(V8(EDGES(UnitTestsAhuacatlEdge, @start, 'any', @examples))) SORT e.what RETURN e.what" + ]; + + queries.forEach(function (q) { + var actual; + actual = getQueryResults(q, {start: "UnitTestsAhuacatlVertex/v3", examples: {what: "v1->v3"} }); + assertEqual(actual, [ "v1->v3" ]); + + actual = getQueryResults(q, {start: "UnitTestsAhuacatlVertex/v3", examples: [{what: "v1->v3"}, {what: "v3->v6"}] }); + assertEqual(actual, [ "v1->v3", "v3->v6"]); + + actual = getQueryResults(q, {start: "UnitTestsAhuacatlVertex/v3", examples: [] }); + assertEqual(actual, [ "v1->v3", "v2->v3", "v3->v4", "v3->v6", "v3->v7", "v6->v3", "v7->v3" ]); + + actual = getQueryResults(q, {start: "UnitTestsAhuacatlVertex/v3", examples: null }); + assertEqual(actual, [ "v1->v3", "v2->v3", "v3->v4", "v3->v6", "v3->v7", "v6->v3", "v7->v3" ]); + + actual = getQueryResults(q, {start: "UnitTestsAhuacatlVertex/v3", examples: {non: "matchable"} }); + assertEqual(actual, [ ]); + + actual = getQueryResults(q, {start: "UnitTestsAhuacatlVertex/v3", examples: [{what: "v1->v3"}, {non: "matchable"}] }); + assertEqual(actual, [ "v1->v3" ]); + + actual = getQueryResults(q, {start: "UnitTestsAhuacatlVertex/v3", examples: [{what: "v1->v3"}, {non: "matchable"}] }); + assertEqual(actual, [ "v1->v3" ]); + + actual = getQueryResults(q, {start: "UnitTestsAhuacatlVertex/v3", examples: ["UnitTestsAhuacatlEdge/v1v3", {what: "v3->v6" } ] }); + assertEqual(actual, [ "v3->v6" ]); + }); + } }; }