diff --git a/Documentation/Examples/aqltraversal b/Documentation/Examples/aqltraversal new file mode 100644 index 0000000000..de0b154f39 --- /dev/null +++ b/Documentation/Examples/aqltraversal @@ -0,0 +1,8 @@ +TRAVERSE(friends, friendrelations, "friends/john", "outbound", { + strategy: "depthfirst", + order: "postorder", + itemOrder: "backward", + maxDepth: 6, + trackPaths: true +}) + diff --git a/arangod/Ahuacatl/ahuacatl-functions.c b/arangod/Ahuacatl/ahuacatl-functions.c index 1424e2dcfa..317375842b 100644 --- a/arangod/Ahuacatl/ahuacatl-functions.c +++ b/arangod/Ahuacatl/ahuacatl-functions.c @@ -327,6 +327,49 @@ static bool EqualName (TRI_associative_pointer_t* array, /// @{ //////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +/// @brief check if we have a matching restriction we can use to optimise +/// a PATHS query +//////////////////////////////////////////////////////////////////////////////// + +static bool CheckPathRestriction (TRI_aql_field_access_t* fieldAccess, + TRI_aql_context_t* const context, + TRI_aql_node_t* vertexCollection, + const char* lookFor, + char* name, + const size_t n) { + size_t len; + + assert(fieldAccess); + assert(lookFor); + + len = strlen(lookFor); + if (len == 0) { + return false; + } + + if (n > fieldAccess->_variableNameLength + len && + memcmp((void*) lookFor, (void*) name, len) == 0) { + // we'll now patch the collection hint + TRI_aql_collection_hint_t* hint; + + // field name is collection.source.XXX, e.g. users.source._id + LOG_DEBUG("optimising PATHS() field access %s", fieldAccess->_fullName); + + // we can now modify this fieldaccess in place to collection.XXX, e.g. users._id + // copy trailing \0 byte as well + memmove(name, name + len - 1, n - fieldAccess->_variableNameLength - len + 2); + + // attach the modified fieldaccess to the collection + hint = (TRI_aql_collection_hint_t*) (TRI_AQL_NODE_DATA(vertexCollection)); + hint->_ranges = TRI_AddAccessAql(context, hint->_ranges, fieldAccess); + + return true; + } + + return false; +} + //////////////////////////////////////////////////////////////////////////////// /// @brief optimise callback function for PATHS() AQL function //////////////////////////////////////////////////////////////////////////////// @@ -340,8 +383,6 @@ static void OptimisePaths (const TRI_aql_node_t* const fcallNode, TRI_aql_node_t* direction; char* directionValue; char* name; - const char* lookFor; - size_t len; size_t n; args = TRI_AQL_NODE_MEMBER(fcallNode, 0); @@ -366,37 +407,17 @@ static void OptimisePaths (const TRI_aql_node_t* const fcallNode, // try to optimise the vertex collection access if (TRI_EqualString(directionValue, "outbound")) { - lookFor = ".source."; - len = strlen(lookFor); + CheckPathRestriction(fieldAccess, context, vertexCollection, ".source.", name, n); } else if (TRI_EqualString(directionValue, "inbound")) { - lookFor = ".destination."; - len = strlen(lookFor); + CheckPathRestriction(fieldAccess, context, vertexCollection, ".destination.", name, n); } - else { - // "any" will not be optimised - lookFor = NULL; - len = 0; - } - - if (len > 0 && - n > fieldAccess->_variableNameLength + len && - memcmp((void*) lookFor, (void*) name, len) == 0) { - // we'll now patch the collection hint - TRI_aql_collection_hint_t* hint; - - // field name is collection.source.XXX, e.g. users.source._id - LOG_DEBUG("optimising PATHS() field access %s", fieldAccess->_fullName); - - // we can now modify this fieldaccess in place to collection.XXX, e.g. users._id - // copy trailing \0 byte as well - memmove(name, name + len - 1, n - fieldAccess->_variableNameLength - len + 2); - - // attach the modified fieldaccess to the collection - hint = (TRI_aql_collection_hint_t*) (TRI_AQL_NODE_DATA(vertexCollection)); - hint->_ranges = TRI_AddAccessAql(context, hint->_ranges, fieldAccess); + else if (TRI_EqualString(directionValue, "any")) { + CheckPathRestriction(fieldAccess, context, vertexCollection, ".source.", name, n); + CheckPathRestriction(fieldAccess, context, vertexCollection, ".destination.", name, n); } + // check if we have a filter on LENGTH(edges) if (args->_members._length <= 4 && TRI_EqualString(name, ".edges.LENGTH()")) { // length restriction, can only be applied if length parameters are not already set @@ -603,6 +624,7 @@ TRI_associative_pointer_t* TRI_InitialiseFunctionsAql (void) { // graph functions REGISTER_FUNCTION("PATHS", "GRAPH_PATHS", false, false, "c,h|s,b", &OptimisePaths); + REGISTER_FUNCTION("TRAVERSE", "GRAPH_TRAVERSE", false, false, "h,h,s,s,a", NULL); // misc functions REGISTER_FUNCTION("FAIL", "FAIL", false, false, "|s", NULL); // FAIL is non-deterministic, otherwise query optimisation will fail! diff --git a/arangod/Documentation/aql.dox b/arangod/Documentation/aql.dox index 2f9c911a9c..3de4ff6926 100644 --- a/arangod/Documentation/aql.dox +++ b/arangod/Documentation/aql.dox @@ -959,7 +959,7 @@ /// that contain a word starting with the prefix @LIT{cent} and that also contain a word /// starting with the prefix @LIT{subst}. /// -/// If multiple search words (or prefixes) are given, then by default the results will +/// If multiple search words (or prefixes) are given, then by default the results will be /// AND-combined, meaning only the logical intersection of all searches will be returned. /// It is also possible to combine partial results with a logical OR, and with a logical NOT: /// @@ -997,15 +997,55 @@ /// /// The result of the function is a list of paths. Paths of length 0 will also be returned. Each /// path is a document consisting of the following attributes: -/// - @FA{vertices}: list of vertices visited along the path -/// - @FA{edges}: list of edges visited along the path (might be empty) -/// - @FA{source}: start vertex of path -/// - @FA{destination}: destination vertex of path +/// - @LIT{vertices}: list of vertices visited along the path +/// - @LIT{edges}: list of edges visited along the path (might be empty) +/// - @LIT{source}: start vertex of path +/// - @LIT{destination}: destination vertex of path /// /// Example calls: /// /// @verbinclude aqlpaths /// +/// - @FN{TRAVERSE(@FA{vertexcollection}\, @FA{edgecollection}\, @FA{startVertex}\, @FA{direction}\, @FA{options})}: +/// traverses the graph described by @FA{vertexcollection} and @FA{edgecollection}, +/// starting at the vertex identified by id @FA{startVertex}. Vertex connectivity is +/// specified by the @FA{direction} parameter: +/// - @LIT{outbound}: vertices are connected in @LIT{_from} to @LIT{_to} order +/// - @LIT{inbound}: vertices are connected in @LIT{_to} to @LIT{_from} order +/// +/// Additional options for the traversal can be provided via the @FA{options} document: +/// - @LIT{strategy}: defines the traversal strategy. Possible values are @LIT{depthfirst} +/// and @LIT{breadthfirst}. Defaults to @LIT{depthfirst} +/// - @LIT{order}: defines the traversal order: Possible values are @LIT{preorder} and +/// @LIT{postorder}. Defaults to @LIT{preorder} +/// - @LIT{itemOrder}: Defines the level item order. Can be @LIT{forward} or +/// @LIT{backward}. Defaults to @LIT{forward} +/// - @LIT{maxDepth}: Maximum traversal depth. Defaults to unbounded +/// - @LIT{trackPaths}: if @LIT{true}, the paths encountered during the traversal will +/// also be returned along with each traversed vertex. If @LIT{false}, only the +/// encountered vertices will be returned. +/// - @LIT[uniqueness}: an optional document with the following properties: +/// - @LIT{vertices}: +/// - @LIT{none}: no vertex uniqueness is enforced +/// - @LIT{global}: a vertex may be visited at most once +/// - @LIT{path}: a vertex is visited only if not already contained in the current +/// traversal path +/// - @LIT{edges}: +/// - @LIT{none}: no edge uniqueness is enforced +/// - @LIT{global}: an edge may be visited at most once +/// - @LIT{path}: an edge is visited only if not already contained in the current +/// traversal path +/// +/// The result of the TRAVERSE function is a list of traversed points. Each point is a +/// document consisting of the following properties: +/// - @LIT{vertex}: the vertex at the traversal point +/// - @LIT{path}: The path history for the traversal point. The path is a document with the +/// properties @LIT{vertices} and @LIT{edges}, which are both lists. +/// +/// Example calls: +/// +/// @verbinclude aqltraversal +/// /// @subsubsection AqlFunctionsControl Control flow functions /// /// AQL offers the following functions to let the user control the flow of operations: diff --git a/arangod/V8Server/v8-vocbase.cpp b/arangod/V8Server/v8-vocbase.cpp index 1c7c641257..fe642a95e2 100644 --- a/arangod/V8Server/v8-vocbase.cpp +++ b/arangod/V8Server/v8-vocbase.cpp @@ -2895,7 +2895,7 @@ static v8::Handle JS_UpgradeVocbaseCol (v8::Arguments const& argv) { memset(&newMarker, 0, newMarkerSize); - sprintf(didBuffer,"%d", (unsigned int) oldMarker->_did); + sprintf(didBuffer,"%llu", (unsigned long long) oldMarker->_did); keySize = strlen(didBuffer) + 1; keyBodySize = TRI_DF_ALIGN_BLOCK(keySize); keyBody = (char*) TRI_Allocate(TRI_CORE_MEM_ZONE, keyBodySize, true); @@ -2949,9 +2949,9 @@ static v8::Handle JS_UpgradeVocbaseCol (v8::Arguments const& argv) { memset(&newMarker, 0, newMarkerSize); - sprintf(didBuffer,"%d", (unsigned int) oldMarker->base._did); - sprintf(toDidBuffer,"%d", (unsigned int) oldMarker->_toDid); - sprintf(fromDidBuffer,"%d", (unsigned int) oldMarker->_fromDid); + sprintf(didBuffer,"%llu", (unsigned long long) oldMarker->base._did); + sprintf(toDidBuffer,"%llu", (unsigned long long) oldMarker->_toDid); + sprintf(fromDidBuffer,"%llu", (unsigned long long) oldMarker->_fromDid); keySize = strlen(didBuffer) + 1; toSize = strlen(toDidBuffer) + 1; @@ -3008,7 +3008,7 @@ static v8::Handle JS_UpgradeVocbaseCol (v8::Arguments const& argv) { memset(&newMarker, 0, newMarkerSize); - sprintf(didBuffer,"%d", (unsigned int) oldMarker->_did); + sprintf(didBuffer,"%llu", (unsigned long long) oldMarker->_did); keySize = strlen(didBuffer) + 1; keyBodySize = TRI_DF_ALIGN_BLOCK(keySize); keyBody = (char*) TRI_Allocate(TRI_CORE_MEM_ZONE, keyBodySize, true); diff --git a/arangod/VocBase/datafile.c b/arangod/VocBase/datafile.c index 223347b251..07ce955f2b 100644 --- a/arangod/VocBase/datafile.c +++ b/arangod/VocBase/datafile.c @@ -678,10 +678,10 @@ static TRI_datafile_t* OpenDatafile (char const* filename, bool ignoreErrors) { // check the maximal size if (size > header._maximalSize) { - LOG_WARNING("datafile '%s' has size '%u', but maximal size is '%u'", - filename, - (unsigned int) size, - (unsigned int) header._maximalSize); + LOG_DEBUG("datafile '%s' has size '%u', but maximal size is '%u'", + filename, + (unsigned int) size, + (unsigned int) header._maximalSize); } // map datafile into memory diff --git a/arangod/VocBase/key-generator.c b/arangod/VocBase/key-generator.c index ab279eb513..86456b4fd9 100644 --- a/arangod/VocBase/key-generator.c +++ b/arangod/VocBase/key-generator.c @@ -217,7 +217,7 @@ static int RevisionKey (TRI_key_generator_t* const generator, current += TRI_StringUInt64InPlace(revision, current); } else { - char numBuffer[22]; + char numBuffer[22]; // a uint64 cannot be longer than this size_t length; length = TRI_StringUInt64InPlace(revision, (char*) &numBuffer); diff --git a/js/client/client.js b/js/client/client.js index 7581c9cc4b..bc24cc12c6 100755 --- a/js/client/client.js +++ b/js/client/client.js @@ -1,10 +1,4 @@ -/*jslint indent: 2, - nomen: true, - maxlen: 100, - sloppy: true, - vars: true, - white: true, - plusplus: true */ +/*jslint indent: 2, nomen: true, maxlen: 100, sloppy: true, vars: true, white: true, plusplus: true */ /*global require, arango, db, edges, Module */ //////////////////////////////////////////////////////////////////////////////// @@ -1109,6 +1103,11 @@ function ArangoCollection (database, data) { //////////////////////////////////////////////////////////////////////////////// ArangoCollection.prototype._documenturl = function (id) { + var s = id.split("/"); + + if (s.length == 1) { + return this._database._documenturl(this.name() + "/" + id, this.name()); + } return this._database._documenturl(id, this.name()); }; @@ -1615,6 +1614,78 @@ function ArangoCollection (database, data) { this._type = requestResult['type']; }; +//////////////////////////////////////////////////////////////////////////////// +/// @brief iterators over some elements of a collection +//////////////////////////////////////////////////////////////////////////////// + + ArangoCollection.prototype.iterate = function (iterator, options) { + var probability = 1.0; + var limit = null; + var stmt; + var cursor; + var pos; + + if (options !== undefined) { + if (options.hasOwnProperty("probability")) { + probability = options.probability; + } + + if (options.hasOwnProperty("limit")) { + limit = options.limit; + } + } + + if (limit === null) { + if (probability >= 1.0) { + cursor = this.all(); + } + else { + stmt = internal.sprintf("FOR d IN %s FILTER rand() >= @prob RETURN d", this.name()); + stmt = internal.db._createStatement({ query: stmt }); + + if (probability < 1.0) { + stmt.bind("prob", probability); + } + + cursor = stmt.execute(); + } + } + else { + if (typeof limit !== "number") { + var error = new ArangoError(); + error.errorNum = internal.errors.ERROR_ILLEGAL_NUMBER.code; + error.errorMessage = "expecting a number, got " + String(limit); + + throw error; + } + + if (probability >= 1.0) { + cursor = this.all().limit(limit); + } + else { + stmt = internal.sprintf("FOR d IN %s FILTER rand() >= @prob LIMIT %d RETURN d", + this.name(), limit); + stmt = internal.db._createStatement({ query: stmt }); + + if (probability < 1.0) { + stmt.bind("prob", probability); + } + + cursor = stmt.execute(); + } + } + + pos = 0; + + while (cursor.hasNext()) { + var document = cursor.next(); + + iterator(document, pos); + + pos++; + } + }; + //////////////////////////////////////////////////////////////////////////////// /// @} //////////////////////////////////////////////////////////////////////////////// @@ -1657,7 +1728,7 @@ function ArangoCollection (database, data) { } if (rev === null) { - requestResult = this._database._connection.GET(this._documenturl(this._name + "/" + id)); + requestResult = this._database._connection.GET(this._documenturl(id)); } else { requestResult = this._database._connection.GET(this._documenturl(id), diff --git a/js/common/bootstrap/module-internal.js b/js/common/bootstrap/module-internal.js index b38128bbbd..d1e14e0b11 100644 --- a/js/common/bootstrap/module-internal.js +++ b/js/common/bootstrap/module-internal.js @@ -542,6 +542,15 @@ internal.print = internal.printBrowser; } +//////////////////////////////////////////////////////////////////////////////// +/// @brief global print +//////////////////////////////////////////////////////////////////////////////// + + internal.printf = function () { + var text = internal.sprintf.apply(internal.springf, arguments); + internal.output(text); + } + //////////////////////////////////////////////////////////////////////////////// /// @brief start pager //////////////////////////////////////////////////////////////////////////////// diff --git a/js/common/modules/org/arangodb/graph.js b/js/common/modules/org/arangodb/graph.js index a0b0562e83..a02518e0d3 100644 --- a/js/common/modules/org/arangodb/graph.js +++ b/js/common/modules/org/arangodb/graph.js @@ -1,8 +1,4 @@ -/*jslint indent: 2, - nomen: true, - maxlen: 100, - sloppy: true, - plusplus: true */ +/*jslint indent: 2, nomen: true, maxlen: 100, sloppy: true, plusplus: true */ /*global require, WeakDictionary, exports */ //////////////////////////////////////////////////////////////////////////////// @@ -104,7 +100,7 @@ findOrCreateEdgeCollectionByName = function (name) { /// @brief constructs a new edge object //////////////////////////////////////////////////////////////////////////////// -function Edge(graph, id) { +function Edge (graph, id) { var properties = graph._edges.document(id); this._graph = graph; diff --git a/js/common/modules/org/arangodb/graph/traversal.js b/js/common/modules/org/arangodb/graph/traversal.js index 088b23d127..6de18e4711 100644 --- a/js/common/modules/org/arangodb/graph/traversal.js +++ b/js/common/modules/org/arangodb/graph/traversal.js @@ -40,59 +40,61 @@ var internal = require("internal"); /// @brief traversal constructor //////////////////////////////////////////////////////////////////////////////// -function ArangoTraverser (edgeCollection, properties) { - // check edgeCollection - if (edgeCollection == undefined || edgeCollection == null) { - throw "invalid edgeCollection"; - } - - this._edgeCollection = edgeCollection; - - if (typeof properties !== "object") { - throw "invalid properties"; - } - - this._order = properties.visitOrder || ArangoTraverser.PRE_ORDER; - this._itemOrder = properties.itemOrder || ArangoTraverser.FORWARD; - - // check the visitation strategy - if (properties.strategy !== ArangoTraverser.BREADTH_FIRST && - properties.strategy !== ArangoTraverser.DEPTH_FIRST) { - throw "invalid strategy"; - } - this._strategy = properties.strategy; - - // initialise uniqueness check attributes - this._uniqueness = { - vertices: properties.uniqueness.vertices || ArangoTraverser.UNIQUE_NONE, - edges: properties.uniqueness.edges || ArangoTraverser.UNIQUE_NONE +function ArangoTraverser (config) { + var defaults = { + order: ArangoTraverser.PRE_ORDER, + itemOrder: ArangoTraverser.FORWARD, + strategy: ArangoTraverser.DEPTH_FIRST, + uniqueness: { + vertices: ArangoTraverser.UNIQUE_NONE, + edges: ArangoTraverser.UNIQUE_NONE + }, + visitor: TrackingVisitor, + filter: VisitAllFilter, + expander: CollectionOutboundExpander }; - // callbacks - // visitor - this._visitor = properties.visitor; + if (typeof config !== "object") { + throw "invalid configuration"; + } - // filter - this._filter = properties.filter || function () { - return { - visit: true, - expand: true - }; - }; + // apply defaults + for (var d in defaults) { + if (! defaults.hasOwnProperty(d)) { + continue; + } - this._expander = properties.expander || OutboundExpander; + if (! config.hasOwnProperty(d)) { + config[d] = defaults[d]; + } + } - if (typeof this._visitor !== "function") { + if (typeof config.visitor !== "function") { throw "invalid visitor"; } - - if (typeof this._filter !== "function") { + + if (Array.isArray(config.filter)) { + config.filter.forEach( function (f) { + if (typeof f !== "function") { + throw "invalid filter"; + } + }); + var innerFilters = config.filter.slice() + var combinedFilter = function(config, vertex, path) { + return CombineFilters(innerFilters, config, vertex, path); + }; + config.filter = combinedFilter; + } + + if (typeof config.filter !== "function") { throw "invalid filter"; } - if (typeof this._expander !== "function") { + if (typeof config.expander !== "function") { throw "invalid expander"; } + + this.config = config; } //////////////////////////////////////////////////////////////////////////////// @@ -112,25 +114,23 @@ function ArangoTraverser (edgeCollection, properties) { /// @brief execute the traversal //////////////////////////////////////////////////////////////////////////////// -ArangoTraverser.prototype.traverse = function (startVertex, context) { +ArangoTraverser.prototype.traverse = function (result, startVertex) { // check the start vertex if (startVertex == undefined || startVertex == null) { throw "invalid startVertex specified for traversal"; } - // set user defined context - this._context = context; - - // run the traversal + // get the traversal strategy var strategy; - if (this._strategy === ArangoTraverser.BREADTH_FIRST) { - strategy = BreadthFirstSearch; + if (this.config.strategy === ArangoTraverser.BREADTH_FIRST) { + strategy = BreadthFirstSearch(); } else { - strategy = DepthFirstSearch; + strategy = DepthFirstSearch(); } - - strategy().run(this, startVertex); + + // run the traversal + strategy.run(this.config, result, startVertex); }; //////////////////////////////////////////////////////////////////////////////// @@ -146,6 +146,49 @@ ArangoTraverser.prototype.traverse = function (startVertex, context) { /// @{ //////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////// +/// @brief parse a filter result +//////////////////////////////////////////////////////////////////////////////// + +function ParseFilterResult (args) { + var result = { + visit: true, + expand: true + }; + + function processArgument (arg) { + if (arg == undefined || arg == null) { + return; + } + + if (typeof(arg) === 'string') { + if (arg === ArangoTraverser.EXCLUDE) { + result.visit = false; + return; + } + else if (arg === ArangoTraverser.PRUNE) { + result.expand = false; + return; + } + else if (arg === '') { + return; + } + } + else if (Array.isArray(arg)) { + for (var i = 0; i < arg.length; ++i) { + processArgument(arg[i]); + } + return; + } + + throw "invalid filter result"; + } + + processArgument(args); + + return result; +} + //////////////////////////////////////////////////////////////////////////////// /// @brief apply the uniqueness checks //////////////////////////////////////////////////////////////////////////////// @@ -174,19 +217,21 @@ function CheckUniqueness (uniqueness, visited, vertex, edge) { /// @brief check if we must process items in reverse order //////////////////////////////////////////////////////////////////////////////// -function CheckReverse (traverser) { - if (traverser._order === ArangoTraverser.POST_ORDER) { +function CheckReverse (config) { + if (config.order === ArangoTraverser.POST_ORDER) { // post order - if (traverser._itemOrder === ArangoTraverser.FORWARD) { + if (config.itemOrder === ArangoTraverser.FORWARD) { return true; } } - else if (traverser._order === ArangoTraverser.PRE_ORDER) { + else if (config.order === ArangoTraverser.PRE_ORDER) { // pre order - if (traverser._itemOrder === ArangoTraverser.BACKWARD && traverser._strategy === ArangoTraverser.BREADTH_FIRST) { + if (config.itemOrder === ArangoTraverser.BACKWARD && + config.strategy === ArangoTraverser.BREADTH_FIRST) { return true; } - if (traverser._itemOrder === ArangoTraverser.FORWARD && traverser._strategy === ArangoTraverser.DEPTH_FIRST) { + if (config.itemOrder === ArangoTraverser.FORWARD && + config.strategy === ArangoTraverser.DEPTH_FIRST) { return true; } } @@ -200,6 +245,18 @@ function CheckReverse (traverser) { function BreadthFirstSearch () { return { + getPathItems: function (items) { + var visited = { }; + var ignore = items.length - 1; + + items.forEach(function (item, i) { + if (i != ignore) { + visited[item._id] = true; + } + }); + + return visited; + }, createPath: function (items, idx) { var path = { edges: [ ], vertices: [ ] }; @@ -220,13 +277,13 @@ function BreadthFirstSearch () { return path; }, - run: function (traverser, startVertex) { + run: function (config, result, startVertex) { var toVisit = [ { edge: null, vertex: startVertex, parentIndex: -1 } ]; var visited = { edges: { }, vertices: { } }; var index = 0; var step = 1; - var reverse = CheckReverse(traverser); + var reverse = CheckReverse(config); while ((step == 1 && index < toVisit.length) || (step == -1 && index >= 0)) { @@ -237,8 +294,17 @@ function BreadthFirstSearch () { if (current.visit == null) { current.visit = false; + var path = this.createPath(toVisit, index); + // first apply uniqueness check - if (! CheckUniqueness(traverser._uniqueness, visited, vertex, edge)) { + if (config.uniqueness.vertices === ArangoTraverser.UNIQUE_PATH) { + visited.vertices = this.getPathItems(path.vertices); + } + if (config.uniqueness.edges === ArangoTraverser.UNIQUE_PATH) { + visited.edges = this.getPathItems(path.edges); + } + + if (! CheckUniqueness(config.uniqueness, visited, vertex, edge)) { if (index < toVisit.length - 1) { index += step; } @@ -248,12 +314,10 @@ function BreadthFirstSearch () { continue; } - var path = this.createPath(toVisit, index); - - var filterResult = traverser._filter(traverser, vertex, path); - if (traverser._order === ArangoTraverser.PRE_ORDER && filterResult.visit) { + var filterResult = ParseFilterResult(config.filter(config, vertex, path)); + if (config.order === ArangoTraverser.PRE_ORDER && filterResult.visit) { // preorder - traverser._visitor(traverser, vertex, path); + config.visitor(config, result, vertex, path); } else { // postorder @@ -261,7 +325,7 @@ function BreadthFirstSearch () { } if (filterResult.expand) { - var connected = traverser._expander(traverser, vertex, path); + var connected = config.expander(config, vertex, path); if (connected.length > 0) { reverse && connected.reverse(); for (var i = 0; i < connected.length; ++i) { @@ -271,7 +335,7 @@ function BreadthFirstSearch () { } } - if (traverser._order === ArangoTraverser.POST_ORDER) { + if (config.order === ArangoTraverser.POST_ORDER) { if (index < toVisit.length - 1) { index += step; } @@ -281,9 +345,9 @@ function BreadthFirstSearch () { } } else { - if (traverser._order === ArangoTraverser.POST_ORDER && current.visit) { + if (config.order === ArangoTraverser.POST_ORDER && current.visit) { var path = this.createPath(toVisit, index); - traverser._visitor(traverser, vertex, path); + config.visitor(config, result, vertex, path); } index += step; @@ -301,12 +365,19 @@ function BreadthFirstSearch () { function DepthFirstSearch () { return { + getPathItems: function (items) { + var visited = { }; + items.forEach(function (item) { + visited[item._id] = true; + }); + return visited; + }, - run: function (traverser, startVertex) { + run: function (config, result, startVertex) { var toVisit = [ { edge: null, vertex: startVertex, visit: null } ]; var path = { edges: [ ], vertices: [ ] }; var visited = { edges: { }, vertices: { } }; - var reverse = CheckReverse(traverser); + var reverse = CheckReverse(config); while (toVisit.length > 0) { // peek at the top of the stack @@ -319,7 +390,14 @@ function DepthFirstSearch () { current.visit = false; // first apply uniqueness check - if (! CheckUniqueness(traverser._uniqueness, visited, vertex, edge)) { + if (config.uniqueness.vertices === ArangoTraverser.UNIQUE_PATH) { + visited.vertices = this.getPathItems(path.vertices); + } + if (config.uniqueness.edges === ArangoTraverser.UNIQUE_PATH) { + visited.edges = this.getPathItems(path.edges); + } + + if (! CheckUniqueness(config.uniqueness, visited, vertex, edge)) { // skip element if not unique toVisit.pop(); continue; @@ -331,11 +409,10 @@ function DepthFirstSearch () { } path.vertices.push(vertex); - - var filterResult = traverser._filter(traverser, vertex, path); - if (traverser._order === ArangoTraverser.PRE_ORDER && filterResult.visit) { + var filterResult = ParseFilterResult(config.filter(config, vertex, path)); + if (config.order === ArangoTraverser.PRE_ORDER && filterResult.visit) { // preorder visit - traverser._visitor(traverser, vertex, path); + config.visitor(config, result, vertex, path); } else { // postorder. mark the element visitation flag because we'll got to check it later @@ -344,7 +421,7 @@ function DepthFirstSearch () { // expand the element's children? if (filterResult.expand) { - var connected = traverser._expander(traverser, vertex, path); + var connected = config.expander(config, vertex, path); if (connected.length > 0) { reverse && connected.reverse(); for (var i = 0; i < connected.length; ++i) { @@ -356,9 +433,9 @@ function DepthFirstSearch () { } else { // we have already seen this element - if (traverser._order === ArangoTraverser.POST_ORDER && current.visit) { + if (config.order === ArangoTraverser.POST_ORDER && current.visit) { // postorder visitation - traverser._visitor(traverser, vertex, path); + config.visitor(config, result, vertex, path); } // pop the element from the stack @@ -379,32 +456,181 @@ function DepthFirstSearch () { /// @brief default outbound expander function //////////////////////////////////////////////////////////////////////////////// -function OutboundExpander (traverser, vertex, path) { +function CollectionOutboundExpander (config, vertex, path) { var connections = [ ]; + var outEdges = config.edgeCollection.outEdges(vertex._id); + + if (outEdges.length > 1 && config.sort) { + outEdges.sort(config.sort); + } - this._edgeCollection.outEdges(vertex._id).forEach(function (edge) { - var vertex = internal.db._document(edge._to); - connections.push({ edge: edge, vertex: vertex }); + outEdges.forEach(function (edge) { + try { + var vertex = internal.db._document(edge._to); + connections.push({ edge: edge, vertex: vertex }); + } + catch (e) { + // continue even in the face of non-existing documents + } }); return connections; -}; +} //////////////////////////////////////////////////////////////////////////////// /// @brief default inbound expander function //////////////////////////////////////////////////////////////////////////////// -function InboundExpander (traverser, vertex, path) { +function CollectionInboundExpander (config, vertex, path) { var connections = [ ]; + var inEdges = config.edgeCollection.inEdges(vertex._id); + + if (inEdges.length > 1 && config.sort) { + inEdges.sort(config.sort); + } - this._edgeCollection.inEdges(vertex._id).forEach(function (edge) { - var vertex = internal.db._document(edge._from); - connections.push({ edge: edge, vertex: vertex }); + inEdges.forEach(function (edge) { + try { + var vertex = internal.db._document(edge._from); + connections.push({ edge: edge, vertex: vertex }); + } + catch (e) { + // continue even in the face of non-existing documents + } }); - + return connections; +} + +//////////////////////////////////////////////////////////////////////////////// +/// @brief default "any" expander function +//////////////////////////////////////////////////////////////////////////////// + +function CollectionAnyExpander (config, vertex, path) { + var connections = [ ]; + var edges = config.edgeCollection.edges(vertex._id); + + if (edges.length > 1 && config.sort) { + edges.sort(config.sort); + } + + edges.forEach(function (edge) { + try { + var vertex = internal.db._document(edge._from); + connections.push({ edge: edge, vertex: vertex }); + } + catch (e) { + // continue even in the face of non-existing documents + } + }); + + return connections; +} + +/////////////////////////////////////////////////////////////////////////////////////////// +/// @brief default expander that expands all edges labeled with one label in config.labels +/////////////////////////////////////////////////////////////////////////////////////////// + +var ExpandEdgesWithLabels = function (config, vertex, path) { + var result = [ ]; + if (!Array.isArray(config.labels)) { + config.labels = [config.labels]; + } + var edgesList = edges[vertex._id]; + if (edgesList != undefined) { + for (i = 0; i < edgesList.length; ++i) { + if (!!~config.labels.indexOf(edgesList[i].label)) { + result.push({ edge: edgesList[i], vertex: vertices[edgesList[i]._to] }); + } + } + } + return result; }; +//////////////////////////////////////////////////////////////////////////////// +/// @brief default visitor that just tracks every visit +//////////////////////////////////////////////////////////////////////////////// + +function TrackingVisitor (config, result, vertex, path) { + if (! result || ! result.visited) { + return; + } + + function clone (obj) { + if (obj == null || typeof(obj) !== "object") { + return obj; + } + + if (Array.isArray(obj)) { + var copy = []; + for (var i = 0; i < obj.length; ++i) { + copy[i] = clone(obj[i]); + } + } + else if (obj instanceof Object) { + var copy = {}; + for (var attr in obj) { + if (obj.hasOwnProperty(attr)) { + copy[attr] = clone(obj[attr]); + } + } + } + + return copy; + } + + if (result.visited.vertices) { + result.visited.vertices.push(clone(vertex)); + } + if (result.visited.paths) { + result.visited.paths.push(clone(path)); + } +} + +//////////////////////////////////////////////////////////////////////////////// +/// @brief default filter to visit & expand all vertices +//////////////////////////////////////////////////////////////////////////////// + +function VisitAllFilter () { + return ""; +} + +//////////////////////////////////////////////////////////////////////////////// +/// @brief default filter to visit & expand all vertices up to a given depth +//////////////////////////////////////////////////////////////////////////////// + +function MaxDepthFilter (config, vertex, path) { + if (path.vertices.length > config.maxDepth) { + return ArangoTraverser.PRUNE; + } +}; + +//////////////////////////////////////////////////////////////////////////////// +/// @brief default filter to exclude all vertices up to a given depth +//////////////////////////////////////////////////////////////////////////////// + +function MinDepthFilter (config, vertex, path) { + if (path.vertices.length <= config.minDepth) { + return ArangoTraverser.EXCLUDE; + } +}; + +//////////////////////////////////////////////////////////////////////////////// +/// @brief combine an array of filters +//////////////////////////////////////////////////////////////////////////////// + +function CombineFilters (filters, config, vertex, path) { + var result = []; + filters.forEach( function (f) { + var tmp = f(config, vertex, path); + if (!Array.isArray(tmp)) { + tmp = [tmp]; + } + result = result.concat(tmp); + }); + return result; +} + //////////////////////////////////////////////////////////////////////////////// /// @} //////////////////////////////////////////////////////////////////////////////// @@ -472,6 +698,18 @@ ArangoTraverser.FORWARD = 0; ArangoTraverser.BACKWARD = 1; +//////////////////////////////////////////////////////////////////////////////// +/// @brief prune "constant" +//////////////////////////////////////////////////////////////////////////////// + +ArangoTraverser.PRUNE = 'prune'; + +//////////////////////////////////////////////////////////////////////////////// +/// @brief exclude "constant" +//////////////////////////////////////////////////////////////////////////////// + +ArangoTraverser.EXCLUDE = 'exclude'; + //////////////////////////////////////////////////////////////////////////////// /// @} //////////////////////////////////////////////////////////////////////////////// @@ -485,10 +723,16 @@ ArangoTraverser.BACKWARD = 1; /// @{ //////////////////////////////////////////////////////////////////////////////// -exports.Traverser = ArangoTraverser; -exports.OutboundExpander = OutboundExpander; -exports.InboundExpander = InboundExpander; - +exports.Traverser = ArangoTraverser; +exports.CollectionOutboundExpander = CollectionOutboundExpander; +exports.CollectionInboundExpander = CollectionInboundExpander; +exports.CollectionAnyExpander = CollectionAnyExpander; +exports.VisitAllFilter = VisitAllFilter; +exports.TrackingVisitor = TrackingVisitor; +exports.MinDepthFilter = MinDepthFilter; +exports.MaxDepthFilter = MaxDepthFilter; +exports.ExpandEdgesWithLabels = ExpandEdgesWithLabels; + //////////////////////////////////////////////////////////////////////////////// /// @} //////////////////////////////////////////////////////////////////////////////// diff --git a/js/common/modules/statement-basics.js b/js/common/modules/statement-basics.js index 2bc310f1d4..d6d6e574d8 100644 --- a/js/common/modules/statement-basics.js +++ b/js/common/modules/statement-basics.js @@ -84,10 +84,6 @@ ArangoStatement.prototype.bind = function (key, value) { this._bindVars = key; } else if (typeof(key) === "string") { - if (this._bindVars[key] !== undefined) { - throw "redeclaration of bind parameter"; - } - this._bindVars[key] = value; } else if (typeof(key) === "number") { @@ -97,10 +93,6 @@ ArangoStatement.prototype.bind = function (key, value) { throw "invalid bind parameter declaration"; } - if (this._bindVars[strKey] !== undefined) { - throw "redeclaration of bind parameter"; - } - this._bindVars[strKey] = value; } else { diff --git a/js/common/tests/shell-graph-traversal.js b/js/common/tests/shell-graph-traversal.js index 1cee8b9395..b6d4b7da3f 100644 --- a/js/common/tests/shell-graph-traversal.js +++ b/js/common/tests/shell-graph-traversal.js @@ -43,59 +43,86 @@ var traversal = require("org/arangodb/graph/traversal"); // ----------------------------------------------------------------------------- //////////////////////////////////////////////////////////////////////////////// -/// @brief test: Graph Traversal +/// @brief test: Graph tree traversal //////////////////////////////////////////////////////////////////////////////// -function GraphTraversalSuite() { - var vertices; - var edges; +function GraphTreeTraversalSuite () { + //var vertices; + //var edges; + var datasourceWorld; + - var getContext = function () { + var visitor = traversal.TrackingVisitor; + + var filter = function (config, vertex, path) { + var r = [ ]; + if (config.noVisit && config.noVisit[vertex._id]) { + r.push(traversal.Traverser.EXCLUDE); + } + if (config.noExpand && config.noExpand[vertex._id]) { + r.push(traversal.Traverser.PRUNE); + } + return r; + }; + + var expander = function (config, vertex, path) { + var r = [ ]; + + var edgesList = config.datasource.getOutEdges(vertex._id); + if (edgesList != undefined) { + for (i = 0; i < edgesList.length; ++i) { + r.push({ edge: edgesList[i], vertex: config.datasource.vertices[edgesList[i]._to] }); + } + } + return r; + }; + + var getIds = function (data) { + var r = [ ]; + data.forEach(function (item) { + r.push(item._id); + }); + return r; + }; + + var getVisitedPaths = function (data) { + var r = [ ]; + data.forEach(function (item) { + r.push(getIds(item.vertices)); + }); + return r; + }; + + var getResult = function () { return { - visited: [ ], - paths: [ ], - visit: { - "vertices/Antarctica" : false, - "vertices/IE": false - }, - expand: { - "vertices/Africa": false + visited: { + vertices: [ ], + paths: [ ] } }; }; - var visitor = function (traverser, vertex, path) { - var context = traverser._context; - - context.visited.push(vertex._id); - var paths = [ ]; - path.vertices.forEach (function (vertex) { - paths.push(vertex._id); - }); - context.paths.push(paths); - }; - - var filter = function (traverser, vertex, path) { - var context = traverser._context; - + var getConfig = function () { return { - visit: ((context.visit && context.visit[vertex._id] != undefined) ? context.visit[vertex._id] : true), - expand: ((context.expand && context.expand[vertex._id] != undefined) ? context.expand[vertex._id] : true) + uniqueness: { + vertices: traversal.Traverser.UNIQUE_NONE, + edges: traversal.Traverser.UNIQUE_NONE + }, + visitor: visitor, + filter: filter, + expander: expander, + datasource: datasourceWorld, + + noVisit: { + "vertices/Antarctica" : true, + "vertices/IE": true + }, + noExpand: { + "vertices/Africa": true + } }; }; - var expander = function (traverser, vertex, path) { - var result = [ ]; - - var edgesList = edges[vertex._id]; - if (edgesList != undefined) { - for (i = 0; i < edgesList.length; ++i) { - result.push({ edge: edgesList[i], vertex: vertices[edgesList[i]._to] }); - } - } - return result; - } - return { //////////////////////////////////////////////////////////////////////////////// @@ -103,8 +130,9 @@ function GraphTraversalSuite() { //////////////////////////////////////////////////////////////////////////////// setUp : function () { - vertices = { }; - edges = { }; + var worldVertices = { }; + var worldInEdges = { }; + var worldOutEdges = { }; [ "World", "Nothing", "Europe", "Asia", "America", "Australia", "Antarctica", "Africa", "Blackhole", @@ -117,10 +145,10 @@ function GraphTraversalSuite() { _key : key, name : item }; - vertices[vertex._id] = vertex; + worldVertices[vertex._id] = vertex; }); - var connect = function (from, to) { + var connect = function (from, to, inlist, outlist) { var key = from + "x" + to; var edge = { _id : "edges/" + key, @@ -130,29 +158,52 @@ function GraphTraversalSuite() { what : from + "->" + to }; - if (edges[edge._from] == undefined) { - edges[edge._from] = [ ]; + if (outlist[edge._from] == undefined) { + outlist[edge._from] = [ ]; } - edges[edge._from].push(edge); + outlist[edge._from].push(edge); + if (inlist[edge._to] == undefined) { + inlist[edge._to] = [ ]; + } + inlist[edge._to].push(edge); }; - connect("World", "Europe"); - connect("World", "Asia"); - connect("World", "America"); - connect("World", "Australia"); - connect("World", "Africa"); - connect("World", "Antarctica"); - connect("Europe", "DE"); - connect("Europe", "FR"); - connect("Europe", "GB"); - connect("Europe", "IE"); - connect("Asia", "CN"); - connect("Asia", "JP"); - connect("Asia", "TW"); - connect("America", "US"); - connect("America", "MX"); - connect("Australia", "AU"); - connect("Antarctica", "AN"); + connect("World", "Europe", worldInEdges, worldOutEdges); + connect("World", "Asia", worldInEdges, worldOutEdges); + connect("World", "America", worldInEdges, worldOutEdges); + connect("World", "Australia", worldInEdges, worldOutEdges); + connect("World", "Africa", worldInEdges, worldOutEdges); + connect("World", "Antarctica", worldInEdges, worldOutEdges); + connect("Europe", "DE", worldInEdges, worldOutEdges); + connect("Europe", "FR", worldInEdges, worldOutEdges); + connect("Europe", "GB", worldInEdges, worldOutEdges); + connect("Europe", "IE", worldInEdges, worldOutEdges); + connect("Asia", "CN", worldInEdges, worldOutEdges); + connect("Asia", "JP", worldInEdges, worldOutEdges); + connect("Asia", "TW", worldInEdges, worldOutEdges); + connect("America", "US", worldInEdges, worldOutEdges); + connect("America", "MX", worldInEdges, worldOutEdges); + connect("Australia", "AU", worldInEdges, worldOutEdges); + connect("Antarctica", "AN", worldInEdges, worldOutEdges); + + + datasourceWorld = { + inEdges: worldInEdges, + outEdges: worldOutEdges, + vertices: worldVertices, + + getAllEdges: function (vertexId) { + return this.inEdges[vertexId].concat(outEdges[vertex_id]); + }, + + getInEdges: function (vertexId) { + return this.inEdges[vertexId]; + }, + + getOutEdges: function (vertexId) { + return this.outEdges[vertexId]; + } + }; }, //////////////////////////////////////////////////////////////////////////////// @@ -167,23 +218,15 @@ function GraphTraversalSuite() { //////////////////////////////////////////////////////////////////////////////// testDepthFirstPreOrderForward : function () { - var properties = { - strategy: traversal.Traverser.DEPTH_FIRST, - visitOrder: traversal.Traverser.PRE_ORDER, - itemOrder: traversal.Traverser.FORWARD, - uniqueness: { - vertices: traversal.Traverser.UNIQUE_NONE, - edges: traversal.Traverser.UNIQUE_NONE - }, - visitor: visitor, - filter: filter, - expander: expander - }; + var config = getConfig(); + config.strategy = traversal.Traverser.DEPTH_FIRST; + config.order = traversal.Traverser.PRE_ORDER; + config.itemOrder = traversal.Traverser.FORWARD; + + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, config.datasource.vertices["vertices/World"]); - var traverser = new traversal.Traverser("edges", properties); - var context = getContext(); - traverser.traverse(vertices["vertices/World"], context); - var expectedVisits = [ "vertices/World", "vertices/Europe", @@ -203,7 +246,7 @@ function GraphTraversalSuite() { "vertices/AN", ]; - assertEqual(expectedVisits, context.visited); + assertEqual(expectedVisits, getIds(result.visited.vertices)); var expectedPaths = [ [ "vertices/World" ], @@ -224,7 +267,7 @@ function GraphTraversalSuite() { [ "vertices/World", "vertices/Antarctica", "vertices/AN" ] ]; - assertEqual(expectedPaths, context.paths); + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); }, //////////////////////////////////////////////////////////////////////////////// @@ -232,22 +275,14 @@ function GraphTraversalSuite() { //////////////////////////////////////////////////////////////////////////////// testDepthFirstPreOrderBackward : function () { - var properties = { - strategy: traversal.Traverser.DEPTH_FIRST, - visitOrder: traversal.Traverser.PRE_ORDER, - itemOrder: traversal.Traverser.BACKWARD, - uniqueness: { - vertices: traversal.Traverser.UNIQUE_NONE, - edges: traversal.Traverser.UNIQUE_NONE - }, - visitor: visitor, - filter: filter, - expander: expander - }; + var config = getConfig(); + config.strategy = traversal.Traverser.DEPTH_FIRST; + config.order = traversal.Traverser.PRE_ORDER; + config.itemOrder = traversal.Traverser.BACKWARD; - var traverser = new traversal.Traverser("edges", properties); - var context = getContext(); - traverser.traverse(vertices["vertices/World"], context); + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, config.datasource.vertices["vertices/World"]); var expectedVisits = [ "vertices/World", @@ -268,7 +303,7 @@ function GraphTraversalSuite() { "vertices/DE" ]; - assertEqual(expectedVisits, context.visited); + assertEqual(expectedVisits, getIds(result.visited.vertices)); var expectedPaths = [ [ "vertices/World" ], @@ -289,7 +324,7 @@ function GraphTraversalSuite() { [ "vertices/World", "vertices/Europe", "vertices/DE" ] ]; - assertEqual(expectedPaths, context.paths); + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); }, //////////////////////////////////////////////////////////////////////////////// @@ -297,22 +332,14 @@ function GraphTraversalSuite() { //////////////////////////////////////////////////////////////////////////////// testDepthFirstPostOrderForward : function () { - var properties = { - strategy: traversal.Traverser.DEPTH_FIRST, - visitOrder: traversal.Traverser.POST_ORDER, - itemOrder: traversal.Traverser.FORWARD, - uniqueness: { - vertices: traversal.Traverser.UNIQUE_NONE, - edges: traversal.Traverser.UNIQUE_NONE - }, - visitor: visitor, - filter: filter, - expander: expander - }; + var config = getConfig(); + config.strategy = traversal.Traverser.DEPTH_FIRST; + config.order = traversal.Traverser.POST_ORDER; + config.itemOrder = traversal.Traverser.FORWARD; - var traverser = new traversal.Traverser("edges", properties); - var context = getContext(); - traverser.traverse(vertices["vertices/World"], context); + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, config.datasource.vertices["vertices/World"]); var expectedVisits = [ "vertices/DE", @@ -333,7 +360,7 @@ function GraphTraversalSuite() { "vertices/World" ]; - assertEqual(expectedVisits, context.visited); + assertEqual(expectedVisits, getIds(result.visited.vertices)); var expectedPaths = [ [ "vertices/World", "vertices/Europe", "vertices/DE" ], @@ -354,7 +381,7 @@ function GraphTraversalSuite() { [ "vertices/World" ] ]; - assertEqual(expectedPaths, context.paths); + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); }, //////////////////////////////////////////////////////////////////////////////// @@ -362,22 +389,14 @@ function GraphTraversalSuite() { //////////////////////////////////////////////////////////////////////////////// testDepthFirstPostOrderBackward : function () { - var properties = { - strategy: traversal.Traverser.DEPTH_FIRST, - visitOrder: traversal.Traverser.POST_ORDER, - itemOrder: traversal.Traverser.BACKWARD, - uniqueness: { - vertices: traversal.Traverser.UNIQUE_NONE, - edges: traversal.Traverser.UNIQUE_NONE - }, - visitor: visitor, - filter: filter, - expander: expander - }; + var config = getConfig(); + config.strategy = traversal.Traverser.DEPTH_FIRST; + config.order = traversal.Traverser.POST_ORDER; + config.itemOrder = traversal.Traverser.BACKWARD; - var traverser = new traversal.Traverser("edges", properties); - var context = getContext(); - traverser.traverse(vertices["vertices/World"], context); + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, config.datasource.vertices["vertices/World"]); var expectedVisits = [ "vertices/AN", @@ -398,7 +417,7 @@ function GraphTraversalSuite() { "vertices/World" ]; - assertEqual(expectedVisits, context.visited); + assertEqual(expectedVisits, getIds(result.visited.vertices)); var expectedPaths = [ [ "vertices/World", "vertices/Antarctica", "vertices/AN" ], @@ -419,7 +438,7 @@ function GraphTraversalSuite() { [ "vertices/World" ] ]; - assertEqual(expectedPaths, context.paths); + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); }, //////////////////////////////////////////////////////////////////////////////// @@ -427,22 +446,14 @@ function GraphTraversalSuite() { //////////////////////////////////////////////////////////////////////////////// testBreadthFirstPreOrderForward : function () { - var properties = { - strategy: traversal.Traverser.BREADTH_FIRST, - visitOrder: traversal.Traverser.PRE_ORDER, - itemOrder: traversal.Traverser.FORWARD, - uniqueness: { - vertices: traversal.Traverser.UNIQUE_NONE, - edges: traversal.Traverser.UNIQUE_NONE - }, - visitor: visitor, - filter: filter, - expander: expander - }; + var config = getConfig(); + config.strategy = traversal.Traverser.BREADTH_FIRST; + config.order = traversal.Traverser.PRE_ORDER; + config.itemOrder = traversal.Traverser.FORWARD; - var traverser = new traversal.Traverser("edges", properties); - var context = getContext(); - traverser.traverse(vertices["vertices/World"], context); + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, config.datasource.vertices["vertices/World"]); var expectedVisits = [ "vertices/World", @@ -463,7 +474,7 @@ function GraphTraversalSuite() { "vertices/AN" ]; - assertEqual(expectedVisits, context.visited); + assertEqual(expectedVisits, getIds(result.visited.vertices)); var expectedPaths = [ [ "vertices/World" ], @@ -484,7 +495,7 @@ function GraphTraversalSuite() { [ "vertices/World", "vertices/Antarctica", "vertices/AN" ] ]; - assertEqual(expectedPaths, context.paths); + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); }, //////////////////////////////////////////////////////////////////////////////// @@ -492,23 +503,15 @@ function GraphTraversalSuite() { //////////////////////////////////////////////////////////////////////////////// testBreadthFirstPreOrderBackward : function () { - var properties = { - strategy: traversal.Traverser.BREADTH_FIRST, - visitOrder: traversal.Traverser.PRE_ORDER, - itemOrder: traversal.Traverser.BACKWARD, - uniqueness: { - vertices: traversal.Traverser.UNIQUE_NONE, - edges: traversal.Traverser.UNIQUE_NONE - }, - visitor: visitor, - filter: filter, - expander: expander - }; + var config = getConfig(); + config.strategy = traversal.Traverser.BREADTH_FIRST; + config.order = traversal.Traverser.PRE_ORDER; + config.itemOrder = traversal.Traverser.BACKWARD; + + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, config.datasource.vertices["vertices/World"]); - var traverser = new traversal.Traverser("edges", properties); - var context = getContext(); - traverser.traverse(vertices["vertices/World"], context); - var expectedVisits = [ "vertices/World", "vertices/Africa", @@ -528,7 +531,7 @@ function GraphTraversalSuite() { "vertices/DE" ]; - assertEqual(expectedVisits, context.visited); + assertEqual(expectedVisits, getIds(result.visited.vertices)); var expectedPaths = [ [ "vertices/World" ], @@ -549,7 +552,7 @@ function GraphTraversalSuite() { [ "vertices/World", "vertices/Europe", "vertices/DE" ] ]; - assertEqual(expectedPaths, context.paths); + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); }, //////////////////////////////////////////////////////////////////////////////// @@ -557,22 +560,14 @@ function GraphTraversalSuite() { //////////////////////////////////////////////////////////////////////////////// testBreadthFirstPostOrderForward : function () { - var properties = { - strategy: traversal.Traverser.BREADTH_FIRST, - visitOrder: traversal.Traverser.POST_ORDER, - itemOrder: traversal.Traverser.FORWARD, - uniqueness: { - vertices: traversal.Traverser.UNIQUE_NONE, - edges: traversal.Traverser.UNIQUE_NONE - }, - visitor: visitor, - filter: filter, - expander: expander - }; + var config = getConfig(); + config.strategy = traversal.Traverser.BREADTH_FIRST; + config.order = traversal.Traverser.POST_ORDER; + config.itemOrder = traversal.Traverser.FORWARD; - var traverser = new traversal.Traverser("edges", properties); - var context = getContext(); - traverser.traverse(vertices["vertices/World"], context); + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, config.datasource.vertices["vertices/World"]); var expectedVisits = [ "vertices/DE", @@ -593,7 +588,7 @@ function GraphTraversalSuite() { "vertices/World" ]; - assertEqual(expectedVisits, context.visited); + assertEqual(expectedVisits, getIds(result.visited.vertices)); var expectedPaths = [ [ "vertices/World", "vertices/Europe", "vertices/DE" ], @@ -614,7 +609,7 @@ function GraphTraversalSuite() { [ "vertices/World" ] ]; - assertEqual(expectedPaths, context.paths); + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); }, //////////////////////////////////////////////////////////////////////////////// @@ -622,22 +617,14 @@ function GraphTraversalSuite() { //////////////////////////////////////////////////////////////////////////////// testBreadthFirstPostOrderBackward : function () { - var properties = { - strategy: traversal.Traverser.BREADTH_FIRST, - visitOrder: traversal.Traverser.POST_ORDER, - itemOrder: traversal.Traverser.BACKWARD, - uniqueness: { - vertices: traversal.Traverser.UNIQUE_NONE, - edges: traversal.Traverser.UNIQUE_NONE - }, - visitor: visitor, - filter: filter, - expander: expander - }; + var config = getConfig(); + config.strategy = traversal.Traverser.BREADTH_FIRST; + config.order = traversal.Traverser.POST_ORDER; + config.itemOrder = traversal.Traverser.BACKWARD; - var traverser = new traversal.Traverser("edges", properties); - var context = getContext(); - traverser.traverse(vertices["vertices/World"], context); + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, config.datasource.vertices["vertices/World"]); var expectedVisits = [ "vertices/AN", @@ -658,7 +645,7 @@ function GraphTraversalSuite() { "vertices/World" ]; - assertEqual(expectedVisits, context.visited); + assertEqual(expectedVisits, getIds(result.visited.vertices)); var expectedPaths = [ [ "vertices/World", "vertices/Antarctica", "vertices/AN" ], @@ -679,12 +666,776 @@ function GraphTraversalSuite() { [ "vertices/World" ] ]; - assertEqual(expectedPaths, context.paths); - } + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); + }, + //////////////////////////////////////////////////////////////////////////////// + /// @brief test minimal depth filter with depth 0 + //////////////////////////////////////////////////////////////////////////////// + + testMinDepthFilterWithDepth0 : function () { + var config = { + datasource: datasourceWorld, + expander: expander, + filter: traversal.MinDepthFilter, + minDepth: 0 + }; + + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, config.datasource.vertices["vertices/World"]); + + var expectedVisits = [ + "vertices/World", + "vertices/Europe", + "vertices/DE", + "vertices/FR", + "vertices/GB", + "vertices/IE", + "vertices/Asia", + "vertices/CN", + "vertices/JP", + "vertices/TW", + "vertices/America", + "vertices/US", + "vertices/MX", + "vertices/Australia", + "vertices/AU", + "vertices/Africa", + "vertices/Antarctica", + "vertices/AN", + ]; + + assertEqual(expectedVisits, getIds(result.visited.vertices)); + + var expectedPaths = [ + [ "vertices/World"], + [ "vertices/World", "vertices/Europe" ], + [ "vertices/World", "vertices/Europe", "vertices/DE" ], + [ "vertices/World", "vertices/Europe", "vertices/FR" ], + [ "vertices/World", "vertices/Europe", "vertices/GB" ], + [ "vertices/World", "vertices/Europe", "vertices/IE" ], + [ "vertices/World", "vertices/Asia" ], + [ "vertices/World", "vertices/Asia", "vertices/CN" ], + [ "vertices/World", "vertices/Asia", "vertices/JP" ], + [ "vertices/World", "vertices/Asia", "vertices/TW" ], + [ "vertices/World", "vertices/America" ], + [ "vertices/World", "vertices/America", "vertices/US" ], + [ "vertices/World", "vertices/America", "vertices/MX" ], + [ "vertices/World", "vertices/Australia" ], + [ "vertices/World", "vertices/Australia", "vertices/AU" ], + [ "vertices/World", "vertices/Africa" ], + [ "vertices/World", "vertices/Antarctica"], + [ "vertices/World", "vertices/Antarctica", "vertices/AN" ] + ]; + + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); + + }, + + //////////////////////////////////////////////////////////////////////////////// + /// @brief test minimal depth filter with depth 1 + //////////////////////////////////////////////////////////////////////////////// + + testMinDepthFilterWithDepth1 : function () { + var config = { + datasource: datasourceWorld, + expander: expander, + filter: traversal.MinDepthFilter, + minDepth: 1 + }; + + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, config.datasource.vertices["vertices/World"]); + + var expectedVisits = [ + "vertices/Europe", + "vertices/DE", + "vertices/FR", + "vertices/GB", + "vertices/IE", + "vertices/Asia", + "vertices/CN", + "vertices/JP", + "vertices/TW", + "vertices/America", + "vertices/US", + "vertices/MX", + "vertices/Australia", + "vertices/AU", + "vertices/Africa", + "vertices/Antarctica", + "vertices/AN", + ]; + + assertEqual(expectedVisits, getIds(result.visited.vertices)); + + var expectedPaths = [ + [ "vertices/World", "vertices/Europe" ], + [ "vertices/World", "vertices/Europe", "vertices/DE" ], + [ "vertices/World", "vertices/Europe", "vertices/FR" ], + [ "vertices/World", "vertices/Europe", "vertices/GB" ], + [ "vertices/World", "vertices/Europe", "vertices/IE" ], + [ "vertices/World", "vertices/Asia" ], + [ "vertices/World", "vertices/Asia", "vertices/CN" ], + [ "vertices/World", "vertices/Asia", "vertices/JP" ], + [ "vertices/World", "vertices/Asia", "vertices/TW" ], + [ "vertices/World", "vertices/America" ], + [ "vertices/World", "vertices/America", "vertices/US" ], + [ "vertices/World", "vertices/America", "vertices/MX" ], + [ "vertices/World", "vertices/Australia" ], + [ "vertices/World", "vertices/Australia", "vertices/AU" ], + [ "vertices/World", "vertices/Africa" ], + [ "vertices/World", "vertices/Antarctica"], + [ "vertices/World", "vertices/Antarctica", "vertices/AN" ] + ]; + + + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); + + }, + + + //////////////////////////////////////////////////////////////////////////////// + /// @brief test minimal depth filter with depth 2 + //////////////////////////////////////////////////////////////////////////////// + + testMinDepthFilterWithDepth2 : function () { + var config = { + datasource: datasourceWorld, + expander: expander, + filter: traversal.MinDepthFilter, + minDepth: 2 + }; + + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, config.datasource.vertices["vertices/World"]); + + var expectedVisits = [ + "vertices/DE", + "vertices/FR", + "vertices/GB", + "vertices/IE", + "vertices/CN", + "vertices/JP", + "vertices/TW", + "vertices/US", + "vertices/MX", + "vertices/AU", + "vertices/AN", + ]; + + assertEqual(expectedVisits, getIds(result.visited.vertices)); + + var expectedPaths = [ + [ "vertices/World", "vertices/Europe", "vertices/DE" ], + [ "vertices/World", "vertices/Europe", "vertices/FR" ], + [ "vertices/World", "vertices/Europe", "vertices/GB" ], + [ "vertices/World", "vertices/Europe", "vertices/IE" ], + [ "vertices/World", "vertices/Asia", "vertices/CN" ], + [ "vertices/World", "vertices/Asia", "vertices/JP" ], + [ "vertices/World", "vertices/Asia", "vertices/TW" ], + [ "vertices/World", "vertices/America", "vertices/US" ], + [ "vertices/World", "vertices/America", "vertices/MX" ], + [ "vertices/World", "vertices/Australia", "vertices/AU" ], + [ "vertices/World", "vertices/Antarctica", "vertices/AN" ] + ]; + + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); + + }, + + //////////////////////////////////////////////////////////////////////////////// + /// @brief test maximal depth filter with depth 0 + //////////////////////////////////////////////////////////////////////////////// + + testMaxDepthFilterWithDepth0 : function () { + var config = { + datasource: datasourceWorld, + expander: expander, + filter: traversal.MaxDepthFilter, + maxDepth: 0 + }; + + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, config.datasource.vertices["vertices/World"]); + + var expectedVisits = [ + "vertices/World", + ]; + + assertEqual(expectedVisits, getIds(result.visited.vertices)); + + var expectedPaths = [ + [ "vertices/World"] + ]; + + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); + + }, + + //////////////////////////////////////////////////////////////////////////////// + /// @brief test maximal depth filter with depth 1 + //////////////////////////////////////////////////////////////////////////////// + + testMaxDepthFilterWithDepth1 : function () { + var config = { + datasource: datasourceWorld, + expander: expander, + filter: traversal.MaxDepthFilter, + maxDepth: 1 + }; + + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, config.datasource.vertices["vertices/World"]); + + var expectedVisits = [ + "vertices/World", + "vertices/Europe", + "vertices/Asia", + "vertices/America", + "vertices/Australia", + "vertices/Africa", + "vertices/Antarctica", + ]; + + assertEqual(expectedVisits, getIds(result.visited.vertices)); + + var expectedPaths = [ + [ "vertices/World"], + [ "vertices/World", "vertices/Europe" ], + [ "vertices/World", "vertices/Asia" ], + [ "vertices/World", "vertices/America" ], + [ "vertices/World", "vertices/Australia" ], + [ "vertices/World", "vertices/Africa" ], + [ "vertices/World", "vertices/Antarctica"] + ]; + + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); + + }, + + //////////////////////////////////////////////////////////////////////////////// + /// @brief test maximal depth filter with depth 2 + //////////////////////////////////////////////////////////////////////////////// + + testMaxDepthFilterWithDepth2 : function () { + var config = { + datasource: datasourceWorld, + expander: expander, + filter: traversal.MaxDepthFilter, + maxDepth: 2 + }; + + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, config.datasource.vertices["vertices/World"]); + + var expectedVisits = [ + "vertices/World", + "vertices/Europe", + "vertices/DE", + "vertices/FR", + "vertices/GB", + "vertices/IE", + "vertices/Asia", + "vertices/CN", + "vertices/JP", + "vertices/TW", + "vertices/America", + "vertices/US", + "vertices/MX", + "vertices/Australia", + "vertices/AU", + "vertices/Africa", + "vertices/Antarctica", + "vertices/AN", + ]; + + assertEqual(expectedVisits, getIds(result.visited.vertices)); + + var expectedPaths = [ + [ "vertices/World"], + [ "vertices/World", "vertices/Europe" ], + [ "vertices/World", "vertices/Europe", "vertices/DE" ], + [ "vertices/World", "vertices/Europe", "vertices/FR" ], + [ "vertices/World", "vertices/Europe", "vertices/GB" ], + [ "vertices/World", "vertices/Europe", "vertices/IE" ], + [ "vertices/World", "vertices/Asia" ], + [ "vertices/World", "vertices/Asia", "vertices/CN" ], + [ "vertices/World", "vertices/Asia", "vertices/JP" ], + [ "vertices/World", "vertices/Asia", "vertices/TW" ], + [ "vertices/World", "vertices/America" ], + [ "vertices/World", "vertices/America", "vertices/US" ], + [ "vertices/World", "vertices/America", "vertices/MX" ], + [ "vertices/World", "vertices/Australia" ], + [ "vertices/World", "vertices/Australia", "vertices/AU" ], + [ "vertices/World", "vertices/Africa" ], + [ "vertices/World", "vertices/Antarctica"], + [ "vertices/World", "vertices/Antarctica", "vertices/AN" ] + ]; + + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); + + }, + + // ----------------------------------------------------------------------------- + // --SECTION-- combineFilters + // ----------------------------------------------------------------------------- + + //////////////////////////////////////////////////////////////////////////////// + /// @brief test combination of filters + //////////////////////////////////////////////////////////////////////////////// + + testCombineFilters : function () { + + var excluder1 = function(config, vertex, path) { + if (vertex.name && vertex.name === config.exclude1) return "exclude"; + }; + + var excluder2 = function(config, vertex, path) { + if (vertex.name && vertex.name === config.exclude2) return "exclude"; + }; + + var excluder3 = function(config, vertex, path) { + if (vertex.name && vertex.name === config.exclude3) return "exclude"; + }; + + var pruner1 = function(config, vertex, path) { + if (vertex.name && vertex.name === config.prune1) return "prune"; + }; + + var pruner2 = function(config, vertex, path) { + if (vertex.name && vertex.name === config.prune2) return "prune"; + }; + + var config = { + expander: expander, + filter: [ + excluder1, + pruner1, + excluder2, + pruner2, + excluder3 + ], + exclude1: "Europe", + exclude2: "AU", + exclude3: "World", + prune1: "Asia", + prune2: "Europe", + datasource: datasourceWorld + }; + + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, config.datasource.vertices["vertices/World"]); + + var expectedVisits = [ + "vertices/Asia", + "vertices/America", + "vertices/US", + "vertices/MX", + "vertices/Australia", + "vertices/Africa", + "vertices/Antarctica", + "vertices/AN", + ]; + + assertEqual(expectedVisits, getIds(result.visited.vertices)); + + var expectedPaths = [ + [ "vertices/World", "vertices/Asia" ], + [ "vertices/World", "vertices/America" ], + [ "vertices/World", "vertices/America", "vertices/US" ], + [ "vertices/World", "vertices/America", "vertices/MX" ], + [ "vertices/World", "vertices/Australia" ], + [ "vertices/World", "vertices/Africa" ], + [ "vertices/World", "vertices/Antarctica"], + [ "vertices/World", "vertices/Antarctica", "vertices/AN" ] + ]; + + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); + }, + + + //////////////////////////////////////////////////////////////////////////////// + /// @brief test if exclude or prune can be overridden in combined filters + //////////////////////////////////////////////////////////////////////////////// + + testOverrideExcludeAndPruneOfCombinedFilters : function () { + + var excludeAndPrune = function(config, vertex, path) { + if (vertex.name && vertex.name === config.excludeAndPrune) return ["prune", "exclude"]; + }; + + var config = { + expander: expander, + filter: [ + excludeAndPrune, + traversal.VisitAllFilter + ], + excludeAndPrune: "World", + datasource: datasourceWorld + }; + + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, config.datasource.vertices["vertices/World"]); + + var expectedVisits = []; + + assertEqual(expectedVisits, getIds(result.visited.vertices)); + + var expectedPaths = []; + + assertEqual(expectedPaths, getVisitedPaths(result.visited.paths)); + }, + + testFollowEdgesWithLabels : function () { + var config = { + expander: traversal.ExpandEdgesWithLabels, + edgeLabels: ["likes", "hates"] + } + } }; } +// ----------------------------------------------------------------------------- +// --SECTION-- collection graph traversal +// ----------------------------------------------------------------------------- + +//////////////////////////////////////////////////////////////////////////////// +/// @brief test: collection-based graph traversal +//////////////////////////////////////////////////////////////////////////////// + +function CollectionTraversalSuite () { + var vn = "UnitTestsVertices"; + var en = "UnitTestsEdges"; + + var vertexCollection; + var edgeCollection; + + var getResult = function () { + return { + visited: { + vertices: [ ], + paths: [ ] + } + }; + }; + + var getIds = function (data) { + var r = [ ]; + data.forEach(function (item) { + r.push(item._id); + }); + return r; + }; + + return { + +//////////////////////////////////////////////////////////////////////////////// +/// @brief set up +//////////////////////////////////////////////////////////////////////////////// + + setUp : function () { + db._drop(vn); + db._drop(en); + + vertexCollection = db._create(vn); + edgeCollection = db._createEdgeCollection(en); + + [ "A", "B", "C", "D", "E", "F", "G", "H", "I" ].forEach(function (item) { + vertexCollection.save({ _key: item, name: item }); + }); + + [ [ "A", "B" ], [ "B", "C" ], [ "C", "D" ], [ "A", "D" ], [ "D", "E" ], [ "D", "F" ], [ "B", "G" ], [ "B", "I" ], [ "G", "H" ], [ "I", "H"] ].forEach(function (item) { + var l = item[0]; + var r = item[1]; + edgeCollection.save(vn + "/" + l, vn + "/" + r, { _key: l + r, what : l + "->" + r }); + }); + }, + +//////////////////////////////////////////////////////////////////////////////// +/// @brief tear down +//////////////////////////////////////////////////////////////////////////////// + + tearDown : function () { + db._drop(vn); + db._drop(en); + }, + +//////////////////////////////////////////////////////////////////////////////// +/// @brief test outbound expander +//////////////////////////////////////////////////////////////////////////////// + + testOutboundExpander : function () { + var config = { + sort: function (l, r) { return l._key < r._key ? -1 : 1 }, + edgeCollection: edgeCollection + }; + + var expander = traversal.CollectionOutboundExpander; + var connected; + + connected = [ ]; + expander(config, vertexCollection.document(vn + "/A")).forEach(function(item) { + connected.push(item.vertex._key); + }); + + assertEqual([ "B", "D" ], connected); + + connected = [ ]; + expander(config, vertexCollection.document(vn + "/D")).forEach(function(item) { + connected.push(item.vertex._key); + }); + + assertEqual([ "E", "F" ], connected); + + connected = [ ]; + expander(config, vertexCollection.document(vn + "/H")).forEach(function(item) { + connected.push(item.vertex._key); + }); + + assertEqual([ ], connected); + }, + +//////////////////////////////////////////////////////////////////////////////// +/// @brief test inbound expander +//////////////////////////////////////////////////////////////////////////////// + + testInboundExpander : function () { + var config = { + sort: function (l, r) { return l._key < r._key ? -1 : 1 }, + edgeCollection: edgeCollection + }; + + var expander = traversal.CollectionInboundExpander; + var connected; + + connected = [ ]; + expander(config, vertexCollection.document(vn + "/D")).forEach(function(item) { + connected.push(item.vertex._key); + }); + + assertEqual([ "A", "C" ], connected); + + connected = [ ]; + expander(config, vertexCollection.document(vn + "/H")).forEach(function(item) { + connected.push(item.vertex._key); + }); + + assertEqual([ "G", "I" ], connected); + + connected = [ ]; + expander(config, vertexCollection.document(vn + "/A")).forEach(function(item) { + connected.push(item.vertex._key); + }); + + assertEqual([ ], connected); + }, + +//////////////////////////////////////////////////////////////////////////////// +/// @brief test iteration +//////////////////////////////////////////////////////////////////////////////// + + testIterateFullOutbound : function () { + var config = { + edgeCollection: internal.db._collection(en), + strategy: traversal.Traverser.DEPTH_FIRST, + order: traversal.Traverser.PRE_ORDER, + itemOrder: traversal.Traverser.FORWARD, + filter: traversal.VisitAllFilter, + expander: traversal.CollectionOutboundExpander, + + sort: function (l, r) { return l._key < r._key ? -1 : 1 } + }; + + var traverser = new traversal.Traverser(config); + var result = getResult(); + traverser.traverse(result, vertexCollection.document(vn + "/A")); + + var expectedVisits = [ + vn + "/A", + vn + "/B", + vn + "/C", + vn + "/D", + vn + "/E", + vn + "/F", + vn + "/G", + vn + "/H", + vn + "/I", + vn + "/H", + vn + "/D", + vn + "/E", + vn + "/F" + ]; + + assertEqual(expectedVisits, getIds(result.visited.vertices)); + }, + +//////////////////////////////////////////////////////////////////////////////// +/// @brief test iteration +//////////////////////////////////////////////////////////////////////////////// + + testIterateInbound : function () { + var config = { + edgeCollection: internal.db._collection(en), + strategy: traversal.Traverser.DEPTH_FIRST, + order: traversal.Traverser.PRE_ORDER, + itemOrder: traversal.Traverser.FORWARD, + filter: traversal.VisitAllFilter, + expander: traversal.CollectionInboundExpander, + + sort: function (l, r) { return l._key < r._key ? -1 : 1 } + }; + + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, vertexCollection.document(vn + "/F")); + + var expectedVisits = [ + vn + "/F", + vn + "/D", + vn + "/A", + vn + "/C", + vn + "/B", + vn + "/A" + ]; + + assertEqual(expectedVisits, getIds(result.visited.vertices)); + }, + +//////////////////////////////////////////////////////////////////////////////// +/// @brief test iteration +//////////////////////////////////////////////////////////////////////////////// + + testIterateUniqueGlobalVertices : function () { + var config = { + edgeCollection: internal.db._collection(en), + strategy: traversal.Traverser.DEPTH_FIRST, + order: traversal.Traverser.PRE_ORDER, + itemOrder: traversal.Traverser.FORWARD, + uniqueness: { + vertices: traversal.Traverser.UNIQUE_GLOBAL, + edges: traversal.Traverser.UNIQUE_NONE + }, + filter: traversal.VisitAllFilter, + expander: traversal.CollectionOutboundExpander, + + sort: function (l, r) { return l._key < r._key ? -1 : 1 } + }; + + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, vertexCollection.document(vn + "/A")); + + var expectedVisits = [ + vn + "/A", + vn + "/B", + vn + "/C", + vn + "/D", + vn + "/E", + vn + "/F", + vn + "/G", + vn + "/H", + vn + "/I" + ]; + + assertEqual(expectedVisits, getIds(result.visited.vertices)); + }, + +//////////////////////////////////////////////////////////////////////////////// +/// @brief test iteration +//////////////////////////////////////////////////////////////////////////////// + + testIterateUniquePathVertices : function () { + var config = { + edgeCollection: internal.db._collection(en), + strategy: traversal.Traverser.DEPTH_FIRST, + order: traversal.Traverser.PRE_ORDER, + itemOrder: traversal.Traverser.FORWARD, + uniqueness: { + vertices: traversal.Traverser.UNIQUE_PATH, + edges: traversal.Traverser.UNIQUE_NONE + }, + filter: traversal.VisitAllFilter, + expander: traversal.CollectionOutboundExpander, + + sort: function (l, r) { return l._key < r._key ? -1 : 1 } + }; + + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, vertexCollection.document(vn + "/A")); + + var expectedVisits = [ + vn + "/A", + vn + "/B", + vn + "/C", + vn + "/D", + vn + "/E", + vn + "/F", + vn + "/G", + vn + "/H", + vn + "/I", + vn + "/H", + vn + "/D", + vn + "/E", + vn + "/F" + ]; + + assertEqual(expectedVisits, getIds(result.visited.vertices)); + }, + +//////////////////////////////////////////////////////////////////////////////// +/// @brief test iteration +//////////////////////////////////////////////////////////////////////////////// + + testIterateUniqueEdges : function () { + var config = { + edgeCollection: internal.db._collection(en), + strategy: traversal.Traverser.DEPTH_FIRST, + order: traversal.Traverser.PRE_ORDER, + itemOrder: traversal.Traverser.FORWARD, + uniqueness: { + vertices: traversal.Traverser.UNIQUE_NONE, + edges: traversal.Traverser.UNIQUE_GLOBAL + }, + filter: traversal.VisitAllFilter, + expander: traversal.CollectionOutboundExpander, + + sort: function (l, r) { return l._key < r._key ? -1 : 1 } + }; + + var result = getResult(); + var traverser = new traversal.Traverser(config); + traverser.traverse(result, vertexCollection.document(vn + "/A")); + + var expectedVisits = [ + vn + "/A", + vn + "/B", + vn + "/C", + vn + "/D", + vn + "/E", + vn + "/F", + vn + "/G", + vn + "/H", + vn + "/I", + vn + "/H", + vn + "/D" + ]; + + assertEqual(expectedVisits, getIds(result.visited.vertices)); + } + }; +} + + // ----------------------------------------------------------------------------- // --SECTION-- main // ----------------------------------------------------------------------------- @@ -693,7 +1444,8 @@ function GraphTraversalSuite() { /// @brief executes the test suites //////////////////////////////////////////////////////////////////////////////// -jsunity.run(GraphTraversalSuite); +jsunity.run(GraphTreeTraversalSuite); +jsunity.run(CollectionTraversalSuite); return jsunity.done(); diff --git a/js/server/ArangoCollection.js b/js/server/ArangoCollection.js index b5eb079057..eb0658e2c6 100644 --- a/js/server/ArangoCollection.js +++ b/js/server/ArangoCollection.js @@ -242,10 +242,17 @@ if (limit === null) { if (probability >= 1.0) { - stmt = internal.sprintf("FOR d IN %s RETURN d", this.name()); + cursor = this.all(); } else { stmt = internal.sprintf("FOR d IN %s FILTER rand() >= @prob RETURN d", this.name()); + stmt = internal.db._createStatement({ query: stmt }); + + if (probability < 1.0) { + stmt.bind("prob", probability); + } + + cursor = stmt.execute(); } } else { @@ -258,21 +265,21 @@ } if (probability >= 1.0) { - stmt = internal.sprintf("FOR d IN %s LIMIT %d RETURN d", this.name(), limit); + cursor = this.all().limit(limit); } else { stmt = internal.sprintf("FOR d IN %s FILTER rand() >= @prob LIMIT %d RETURN d", this.name(), limit); + stmt = internal.db._createStatement({ query: stmt }); + + if (probability < 1.0) { + stmt.bind("prob", probability); + } + + cursor = stmt.execute(); } } - stmt = internal.db._createStatement({ query: stmt }); - - if (probability < 1.0) { - stmt.bind("prob", probability); - } - - cursor = stmt.execute(); pos = 0; while (cursor.hasNext()) { diff --git a/js/server/ahuacatl.js b/js/server/ahuacatl.js index b56fed480f..83cf9ac21c 100644 --- a/js/server/ahuacatl.js +++ b/js/server/ahuacatl.js @@ -26,6 +26,7 @@ //////////////////////////////////////////////////////////////////////////////// var internal = require("internal"); +var traversal = require("org/arangodb/graph/traversal"); //////////////////////////////////////////////////////////////////////////////// /// @brief type weight used for sorting and comparing @@ -2209,8 +2210,6 @@ function AHUACATL_GRAPH_PATHS () { followCycles : followCycles }; - // TODO: restrict allEdges to edges with certain _from values etc. - var result = [ ]; var n = vertices.length; for (var i = 0; i < n; ++i) { @@ -2304,6 +2303,94 @@ function AHUACATL_GRAPH_SUBNODES (searchAttributes, vertexId, visited, edges, ve return result; } +//////////////////////////////////////////////////////////////////////////////// +/// @brief visitor callback function for traversal +//////////////////////////////////////////////////////////////////////////////// + +function AHUACATL_TRAVERSE_VISITOR (config, result, vertex, path) { + if (config.trackPaths) { + result.push(AHUACATL_CLONE({ vertex: vertex, path: path })); + } + else { + result.push(AHUACATL_CLONE({ vertex: vertex })); + } +} + +//////////////////////////////////////////////////////////////////////////////// +/// @brief traverse a graph +//////////////////////////////////////////////////////////////////////////////// + +function AHUACATL_GRAPH_TRAVERSE () { + var vertexCollection = AHUACATL_COLLECTION(arguments[0]); + var edgeCollection = AHUACATL_COLLECTION(arguments[1]); + var startVertex = arguments[2]; + var direction = arguments[3]; + var params = arguments[4]; + + function validate (value, map) { + if (value == null || value == undefined) { + // use first key from map + for (var m in map) { + if (map.hasOwnProperty(m)) { + value = m; + break; + } + } + } + if (typeof value === 'string') { + value = value.toLowerCase().replace(/-/, ""); + if (map[value] != null) { + return map[value]; + } + } + + AHUACATL_THROW(internal.errors.ERROR_QUERY_FUNCTION_ARGUMENT_TYPE_MISMATCH, "TRAVERSE"); + } + + var config = { + edgeCollection: edgeCollection, + strategy: validate(params.strategy, { + 'depthfirst': traversal.Traverser.DEPTH_FIRST, + 'breadthfirst': traversal.Traverser.BREADTH_FIRST + }), + order: validate(params.order, { + 'preorder': traversal.Traverser.PRE_ORDER, + 'postorder': traversal.Traverser.POST_ORDER + }), + itemOrder: validate(params.itemOrder, { + 'forward': traversal.Traverser.FORWARD, + 'backward': traversal.Traverser.BACKWARD + }), + trackPaths: params.paths || false, + visitor: AHUACATL_TRAVERSE_VISITOR, + maxDepth: params.maxDepth, + filter: params.maxDepth != undefined ? traversal.MaxDepthFilter : VisitAllFilter, + uniqueness: { + vertices: validate(params.uniqueness && params.uniqueness.vertices, { + 'none': traversal.Traverser.UNIQUE_NONE, + 'global': traversal.Traverser.UNIQUE_GLOBAL, + 'path': traversal.Traverser.UNIQUE_PATH + }), + edges: validate(params.uniqueness && params.uniqueness.edges, { + 'none': traversal.Traverser.UNIQUE_NONE, + 'global': traversal.Traverser.UNIQUE_GLOBAL, + 'path': traversal.Traverser.UNIQUE_PATH + }), + }, + expander: validate(direction, { + 'outbound': traversal.CollectionOutboundExpander, + 'inbound': traversal.CollectionInboundExpander, + 'any': traversal.CollectionAnyExpander + }) + }; + + var result = [ ]; + var traverser = new traversal.Traverser(config); + traverser.traverse(result, vertexCollection.document(startVertex)); + + return result; +} + //////////////////////////////////////////////////////////////////////////////// /// @} //////////////////////////////////////////////////////////////////////////////// diff --git a/js/server/tests/ahuacatl-paths.js b/js/server/tests/ahuacatl-paths.js index a4f2354adf..e7d63d7bec 100644 --- a/js/server/tests/ahuacatl-paths.js +++ b/js/server/tests/ahuacatl-paths.js @@ -273,11 +273,89 @@ function ahuacatlQueryPathsTestSuite () { }; } +//////////////////////////////////////////////////////////////////////////////// +/// @brief test suite +//////////////////////////////////////////////////////////////////////////////// + +function ahuacatlQueryTraverseTestSuite () { + var vn = "UnitTestsTraverseVertices"; + var en = "UnitTestsTraverseEdges"; + + var vertices; + var edges; + +//////////////////////////////////////////////////////////////////////////////// +/// @brief execute a given query +//////////////////////////////////////////////////////////////////////////////// + + function executeQuery (query, params) { + var cursor = AHUACATL_RUN(query, params); + if (cursor instanceof ArangoError) { + print(query, cursor.errorMessage); + } + assertFalse(cursor instanceof ArangoError); + return cursor; + } + + return { + +//////////////////////////////////////////////////////////////////////////////// +/// @brief set up +//////////////////////////////////////////////////////////////////////////////// + + setUp : function () { + db._drop(vn); + db._drop(en); + + vertexCollection = db._create(vn); + edgeCollection = db._createEdgeCollection(en); + + [ "A", "B", "C", "D" ].forEach(function (item) { + vertexCollection.save({ _key: item, name: item }); + }); + + [ [ "A", "B" ], [ "B", "C" ], [ "A", "D" ], [ "D", "C" ], [ "C", "A" ] ].forEach(function (item) { + var l = item[0]; + var r = item[1]; + edgeCollection.save(vn + "/" + l, vn + "/" + r, { _key: l + r, what : l + "->" + r }); + }); + }, + +//////////////////////////////////////////////////////////////////////////////// +/// @brief tear down +//////////////////////////////////////////////////////////////////////////////// + + tearDown : function () { + db._drop(vn); + db._drop(en); + }, + +//////////////////////////////////////////////////////////////////////////////// +/// @brief tear down +//////////////////////////////////////////////////////////////////////////////// + + testTraversalDepthFirst : function () { + var config = { + strategy: "depthfirst", + order: "preorder", + itemOrder: "forward", + maxDepth: 2 + }; + + var actual = executeQuery("FOR p IN TRAVERSE(@@v, @@e, '" + vn + "/A', 'outbound', " + JSON.stringify(config) + ") RETURN p.vertex._key", { "@v" : vn, "@e" : en }).getRows(); + + assertEqual([ "A", "B", "C", "D", "C" ], actual); + } + + }; +} + //////////////////////////////////////////////////////////////////////////////// /// @brief executes the test suite //////////////////////////////////////////////////////////////////////////////// jsunity.run(ahuacatlQueryPathsTestSuite); +jsunity.run(ahuacatlQueryTraverseTestSuite); return jsunity.done();