diff --git a/js/common/modules/@arangodb/graph/helpers.js b/js/common/modules/@arangodb/graph/helpers.js index e38795aa3f..e12c47d18c 100644 --- a/js/common/modules/@arangodb/graph/helpers.js +++ b/js/common/modules/@arangodb/graph/helpers.js @@ -27,31 +27,31 @@ /// @author Copyright 2016-2016, ArangoDB GmbH, Cologne, Germany //////////////////////////////////////////////////////////////////////////////// -var internal = require("internal"); +var internal = require('internal'); function makeTree(k, depth, nrShards, subgraph) { // This creates a large graph (vertices and edges), which is a k-ary - // tree of depth . If is "random", then the subgraph - // attribute "sub" will be set randomly in [1..nrShards] as string. - // If is ["prefix", d], then the subgraph attribute "sub" - // will be set to "top" for the layers of depth < d and [1..2^d] as + // tree of depth . If is 'random', then the subgraph + // attribute 'sub' will be set randomly in [1..nrShards] as string. + // If is ['prefix', d], then the subgraph attribute 'sub' + // will be set to 'top' for the layers of depth < d and [1..2^d] as // string for the 2^d subtrees starting at depth d. If is - // "depth", then the subgraph attribute "sub" will be set to the + // 'depth', then the subgraph attribute 'sub' will be set to the // depth of the vertex in the tree as string. Returns an object with - // "vertices" and "edges" attribute. - if (typeof k !== "number" || k > 9 || k < 2) { - throw "bad k"; + // 'vertices' and 'edges' attribute. + if (typeof k !== 'number' || k > 9 || k < 2) { + throw 'bad k'; } - let r = { "vertices": [], "edges": [], k, depth, nrShards, subgraph }; + let r = { 'vertices': [], 'edges': [], k, depth, nrShards, subgraph }; const makeVertices = (which, d) => { let v = {name: which}; - if (subgraph === "random") { - v.sub = "" + Math.floor(Math.random() * nrShards + 1.0); - } else if (subgraph === "depth") { - v.sub = "" + depth; + if (subgraph === 'random') { + v.sub = '' + Math.floor(Math.random() * nrShards + 1.0); + } else if (subgraph === 'depth') { + v.sub = '' + depth; } else { if (d < subgraph[1]) { - v.sub = "top"; + v.sub = 'top'; } else { v.sub = which.substr(0, subgraph[1] + 1); } @@ -63,12 +63,12 @@ function makeTree(k, depth, nrShards, subgraph) { } for (let i = 0; i < k; i++) { let subpos = makeVertices(which + i, d+1); - let e = { _from: pos, _to: subpos, name: "->" + which + i }; + let e = { _from: pos, _to: subpos, name: '->' + which + i }; r.edges.push(e); } return pos; }; - makeVertices("N", 0); + makeVertices('N', 0); return r; } @@ -101,36 +101,36 @@ function makeClusteredGraph(clusterSizes, intDegree, intDegreeDelta, // seed: random seed // Just to make the first few cluster names a bit more interesting: - let clusterNames = ["ABC", "ZTG", "JSH", "LIJ", "GTE", "NSD", "POP", "GZU", - "RRR", "WER", "UIH", "KLO", "QWE", "FAD", "MNB", "VFR"]; + let clusterNames = ['ABC', 'ZTG', 'JSH', 'LIJ', 'GTE', 'NSD', 'POP', 'GZU', + 'RRR', 'WER', 'UIH', 'KLO', 'QWE', 'FAD', 'MNB', 'VFR']; // Just to make the first 10000 vertex names in a cluster a bit more // interesting: - let names1 = ["Max", "Ulf", "Andreas", "Kaveh", "Claudius", "Jan", - "Michael", "Oliver", "Frank", "Willi"]; - let names2 = ["Friedhelm", "Habakuk", "Mickey", "Karl-Heinz", - "Friedrich-Wilhelm", "Hans-Guenter", "Hades", "Callibso", - "Jupiter", "Odin"]; - let names3 = ["Chaplin", "Hardy", "Laurel", "Keaton", "Hallervorden", - "Appelt", "Mittermaier", "Trump", "Queen Elizabeth", - "Merkel"]; + let names1 = ['Max', 'Ulf', 'Andreas', 'Kaveh', 'Claudius', 'Jan', + 'Michael', 'Oliver', 'Frank', 'Willi']; + let names2 = ['Friedhelm', 'Habakuk', 'Mickey', 'Karl-Heinz', + 'Friedrich-Wilhelm', 'Hans-Guenter', 'Hades', 'Callibso', + 'Jupiter', 'Odin']; + let names3 = ['Chaplin', 'Hardy', 'Laurel', 'Keaton', 'Hallervorden', + 'Appelt', 'Mittermaier', 'Trump', 'Queen Elizabeth', + 'Merkel']; function makeName(i) { - let r = names1[i % 10] + "-" + names2[Math.floor(i / 10) % 10] + "_" + + let r = names1[i % 10] + '-' + names2[Math.floor(i / 10) % 10] + '_' + names3[Math.floor(i / 100) % 10]; let z = Math.floor(i / 1000); return z > 0 ? r + z : r; } let rand = new PoorMansRandom(seed); - let r = { "vertices": [], "edges": [], clusterSizes, + let r = { 'vertices': [], 'edges': [], clusterSizes, intDegree, intDegreeDelta, extDegree, seed }; // Make the vertices: let clusters = []; for (let i = 0; i < clusterSizes.length; ++i) { let cluster = []; - let subgraphId = i < clusterNames.length ? clusterNames[i] : "C" + i; + let subgraphId = i < clusterNames.length ? clusterNames[i] : 'C' + i; for (let j = 0; j < clusterSizes[i]; ++j) { - let v = {subgraphId, name: subgraphId + "_" + makeName(j)}; + let v = {subgraphId, name: subgraphId + '_' + makeName(j)}; cluster.push({v, pos: r.vertices.length}); r.vertices.push(v); } @@ -140,7 +140,7 @@ function makeClusteredGraph(clusterSizes, intDegree, intDegreeDelta, // Make the edges: for (let i = 0; i < clusterSizes.length; ++i) { let cluster = clusters[i]; - let subgraphId = i < clusterNames.length ? clusterNames[i] : "C" + i; + let subgraphId = i < clusterNames.length ? clusterNames[i] : 'C' + i; for (let j = 0; j < cluster.length; ++j) { let vv = cluster[j]; let v = vv.v; @@ -150,7 +150,7 @@ function makeClusteredGraph(clusterSizes, intDegree, intDegreeDelta, let toVertexPos = rand.next() % cluster.length; let toVertex = r.vertices[cluster[toVertexPos].pos]; let e = { _from: vv.pos, _to: cluster[toVertexPos].pos, - name: v.name + "->" + toVertex.name }; + name: v.name + '->' + toVertex.name }; r.edges.push(e); } if (rand.nextUnitInterval() < extDegree) { @@ -161,13 +161,12 @@ function makeClusteredGraph(clusterSizes, intDegree, intDegreeDelta, let toVertexPos = rand.next() % clusters[toCluster].length; let toVertex = r.vertices[clusters[toCluster][toVertexPos].pos]; let e = { _from: vv.pos, _to: clusters[toCluster][toVertexPos].pos, - name: v.name + "->" + toVertex.name }; + name: v.name + '->' + toVertex.name }; r.edges.push(e); } } } - console.error("Hugo:", r); return r; } @@ -175,17 +174,17 @@ function makeClusteredGraph(clusterSizes, intDegree, intDegreeDelta, function simulateBreadthFirstSearch(graph, startVertexId, minDepth, maxDepth, uniqueness, mode, dir) { - // is an object with attributes "vertices" and "edges", which + // is an object with attributes 'vertices' and 'edges', which // simply contain the list of vertices (with their _id attributes) // and edges (with _id, _from and _to attributes, all strings). // is the id of the start vertex as a string, // (>= 0) and (>= ) are what they - // say, can be "none", "edgePath", "vertexPath" or - // "vertexGlobal", and can be "vertex", "edge" or "path", which + // say, can be 'none', 'edgePath', 'vertexPath' or + // 'vertexGlobal', and can be 'vertex', 'edge' or 'path', which // produces output containing the last vertex, the last vertex and - // edge, or the complete path is. can be "OUT" or "IN" or "ANY" + // edge, or the complete path is. can be 'OUT' or 'IN' or 'ANY' // for the direction of travel. The result is an object with an array - // of results (according to ) in the "res" component as well as + // of results (according to ) in the 'res' component as well as // an array of indices indicating where the depths start. // First create index tables: @@ -219,17 +218,15 @@ function simulateBreadthFirstSearch(graph, startVertexId, minDepth, maxDepth, break; } - require("internal").print(pos, graph, schreier); let id = schreier[pos].vertex._id; let outEdges = graph.fromTable[id]; let inEdges = graph.toTable[id]; let done = {}; - require("internal").print(id, outEdges, inEdges); let doStep = (edge, vertex) => { let useThisEdge = true; // add more checks here depending on uniqueness mode - if (uniqueness === "edgePath") { + if (uniqueness === 'edgePath') { let p = pos; while (p !== 0) { if (schreier[p].edge._id === edge._id) { @@ -238,7 +235,7 @@ function simulateBreadthFirstSearch(graph, startVertexId, minDepth, maxDepth, } p = schreier[p].pred; } - } else if (uniqueness === "vertexPath") { + } else if (uniqueness === 'vertexPath') { let p = pos; while (p !== null) { if (schreier[p].vertex._id === vertex._id) { @@ -247,7 +244,7 @@ function simulateBreadthFirstSearch(graph, startVertexId, minDepth, maxDepth, } p = schreier[p].pred; } - } else if (uniqueness === "vertexGlobal") { + } else if (uniqueness === 'vertexGlobal') { if (used[vertex._id] === true) { useThisEdge = false; } @@ -257,14 +254,14 @@ function simulateBreadthFirstSearch(graph, startVertexId, minDepth, maxDepth, depth: schreier[pos].depth + 1, pred: pos}); }; - if (dir === "OUT" || dir === "ANY") { + if (dir === 'OUT' || dir === 'ANY') { for (let i = 0; i < outEdges.length; ++i) { let edge = graph.edges[outEdges[i]]; doStep(edge, graph.table[edge._to]); done[edge._id] = true; } } - if (dir === "IN" || dir === "ANY") { + if (dir === 'IN' || dir === 'ANY') { for (let i = 0; i < inEdges.length; ++i) { let edge = graph.edges[inEdges[i]]; if (done[edge._id] === true) { @@ -282,9 +279,9 @@ function simulateBreadthFirstSearch(graph, startVertexId, minDepth, maxDepth, if (schreier[i].depth < minDepth) { continue; } - if (mode === "vertex") { + if (mode === 'vertex') { r.res.push(schreier[i].vertex); - } else if (mode === "edge") { + } else if (mode === 'edge') { r.res.push({vertex: schreier[i].vertex, edge: schreier[i].edge}); } else { let pos = i; @@ -304,10 +301,188 @@ function simulateBreadthFirstSearch(graph, startVertexId, minDepth, maxDepth, return r; } - + +function pathCompare(a, b) { + // Establishes a total order on the set of all paths of the form: + // { vertices: [...], edges: [...] } + // where there is one more vertex than edges, and all vertices and + // edges each have an _id attribute uniquely identifying it. + if (a.vertices.length < b.vertices.length) { + return -1; + } else if (a.vertices.length > b.vertices.length) { + return 1; + } else { + let i = 0; + while (true) { + // Will be left by return if i reaches a.vertices.length - 1 at the + // latest. + if (a.vertices[i]._id < b.vertices[i]._id) { + return -1; + } else if (a.vertices[i]._id > b.vertices[i]._id) { + return 1; + } else { + if (i === a.vertices.length - 1) { + return 0; // all is equal + } + if (a.edges[i]._id < b.edges[i]._id) { + return -1; + } else if (a.edges[i]._id > b.edges[i]._id) { + return 1; + } + // A tie so far, move on. + } + i += 1; + } + } +} + +function checkBFSResult(pattern, toCheck) { + // This tries to check a BFS result against a given pattern. Some + // fuzziness is needed since AQL does not promise the order in which + // edges are followed from a vertex during the traversal. The mode + // 'vertex', 'edge' or 'path' is detected automatically and refers to + // the three possibilities of the query: + // (1) FOR v IN 0..5 ... OPTIONS {'bfs': true} RETURN v + // (2) FOR v, e IN 0..5 ... OPTIONS {'bfs': true} RETURN {vertex: v, edge: e} + // (3) FOR v, e, p IN 0..5 ... OPTIONS {'bfs': true} RETURN p + // The pattern is of the form + // { 'res': [...], 'depths': [...] } + // with two arrays of equal length as returned by simulateBreadthFirstSearch. + // Further filterings may have been applied to the results. + // The following checks are done: + // (1) Number of results are equal + // (2) The set of vertex IDs in each depth is the same in both results + // (3) The set of edge IDs in each depth is the same in both results + // This function returns an object of the form: + // res = { 'error': true/false, 'errorMessage': '' } + // where errorMessage is set iff error is true. This can be used as in: + // assertFalse(res.error, res.errorMessage) + if (pattern.res.length !== toCheck.length) { + return { error: true, errorMessage: 'Number of results does not match.' }; + } + if (pattern.res.length === 0) { + return { error: false }; + } + let guck = pattern.res[0]; + if (typeof guck !== 'object') { + return { error: true, errorMessage: 'Cannot recognize mode.' }; + } + let mode; + if (guck.hasOwnProperty('vertices') && guck.hasOwnProperty('edges')) { + mode = 'path'; + } else if (guck.hasOwnProperty('vertex') && guck.hasOwnProperty('edge')) { + mode = 'edge'; + } else { + mode = 'vertex'; + } + + // Not let the show start, first find the depth levels: + let newDepths = []; + let depth = null; + for (let i = 0; i < pattern.depths.length; ++i) { + if (pattern.depths[i] !== depth) { + if (depth !== null && pattern.depths[i] < depth) { + return { error: true, + errorMessage: 'Depths are decreasing.' }; + } + depth = pattern.depths[i]; + newDepths.push(i); + } + } + newDepths.push(pattern.depths.length); + + // Now check one depth after another: + for (let i = 0; i < newDepths.length - 1; ++i) { + if (mode === 'vertex') { + let tab = {}; + for (let j = newDepths[i]; j < newDepths[i+1]; ++j) { + tab[pattern.res[j]._id] = true; + } + for (let j = newDepths[i]; j < newDepths[i+1]; ++j) { + if (tab[toCheck[j]._id] !== true) { + return { error: true, errorMessage: 'Ids in depth ' + + pattern.depths[j] + ' do not match, "' + + toCheck[j]._id + '" in toCheck is not in pattern.' }; + } + } + } else if (mode === 'edge') { + let vTab = {}; + let eTab = {}; + for (let j = newDepths[i]; j < newDepths[i+1]; ++j) { + vTab[pattern.res[j].vertex._id] = true; + eTab[pattern.res[j].edge._id] = true; + } + for (let j = newDepths[i]; j < newDepths[i+1]; ++j) { + if (vTab[toCheck[j].vertex._id] !== true) { + return { error: true, errorMessage: 'Vertex ids in depth ' + + pattern.depths[j] + ' do not match, "' + + toCheck[j].vertex._id + '" in toCheck is not in pattern.' }; + } + if (eTab[toCheck[j].edge._id] !== true) { + return { error: true, errorMessage: 'Edge ids in depth ' + + pattern.depths[j] + ' do not match, "' + + toCheck[j].edge._id + '" in toCheck is not in pattern.' }; + } + } + } else { // mode === 'path' + let colA = []; + let colB = []; + for (let j = newDepths[i]; j < newDepths[i+1]; ++j) { + colA.push(pattern.res[j]); + colB.push(toCheck[j]); + } + colA.sort(pathCompare); + colB.sort(pathCompare); + for (let j = 0; j < colA.length; ++j) { + if (pathCompare(colA[j], colB[j]) !== 0) { + return { error: true, errorMessage: 'Path sets in depth ' + + pattern.depths[newDepths[i]] + ' do not match, here is a ' + + 'sample: ' + JSON.stringify(colA) + ' (pattern) as opposed '+ + ' to ' + JSON.stringify(colB) + ' (toCheck)' }; + } + } + } + } + + // All done, all is fine: + return { error: false }; +} + +function storeGraph(r, Vname, Ename, Gname) { + // r a graph made by makeTree or makeClusteredGraph, Vname a string for the + // name of the vertex collection, Ename a string for the name of the edge + // collection, Gname is the name of the named graph, returns an object + // with two attributes "graph" with the general graph object and "data" + // with the object to be used by simulateBreadthFirstSearch. + let db = require('internal').db; + let g = require('@arangodb/general-graph'); + db._drop(Vname); + db._drop(Ename); + let V = db._create(Vname); + let E = db._createEdgeCollection(Ename); + let vv = []; + for (let i = 0; i < r.vertices.length; ++i) { + vv.push(V.insert(r.vertices[i])); + } + for (let i = 0; i < r.edges.length; ++i) { + let e = r.edges[i]; + e._from = vv[e._from]._id; + e._to = vv[e._to]._id; + E.insert(e); + } + try { + g._drop(Gname); + } catch (err) { + } + let graph = g._create(Gname); + graph._extendEdgeDefinitions(g._relation(Ename, [Vname], [Vname])); + return { data: { vertices: V.toArray(), edges: E.toArray() }, + graph }; +} + exports.makeTree = makeTree; exports.PoorMansRandom = PoorMansRandom; exports.makeClusteredGraph = makeClusteredGraph; exports.simulateBreadthFirstSearch = simulateBreadthFirstSearch; - - +exports.checkBFSResult = checkBFSResult; +exports.storeGraph = storeGraph;