mirror of https://gitee.com/bigwinds/arangodb
699 lines
26 KiB
JavaScript
699 lines
26 KiB
JavaScript
/* jshint globalstrict:false, strict:false, maxlen: 5000 */
|
|
/* global describe, before, after, it, AQL_EXPLAIN, AQL_EXECUTE, AQL_EXECUTEJSON */
|
|
'use strict';
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / @brief tests for optimizer rules
|
|
// /
|
|
// / @file
|
|
// /
|
|
// / DISCLAIMER
|
|
// /
|
|
// / Copyright 2010-2012 triagens GmbH, Cologne, Germany
|
|
// /
|
|
// / Licensed under the Apache License, Version 2.0 (the "License");
|
|
// / you may not use this file except in compliance with the License.
|
|
// / You may obtain a copy of the License at
|
|
// /
|
|
// / http://www.apache.org/licenses/LICENSE-2.0
|
|
// /
|
|
// / Unless required by applicable law or agreed to in writing, software
|
|
// / distributed under the License is distributed on an "AS IS" BASIS,
|
|
// / WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// / See the License for the specific language governing permissions and
|
|
// / limitations under the License.
|
|
// /
|
|
// / Copyright holder is triAGENS GmbH, Cologne, Germany
|
|
// /
|
|
// / @author Wilfried Goesgens, Michael Hackstein
|
|
// / @author Copyright 2014, triAGENS GmbH, Cologne, Germany
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
const expect = require('chai').expect;
|
|
|
|
const helper = require('@arangodb/aql-helper');
|
|
const isEqual = helper.isEqual;
|
|
const graphModule = require('@arangodb/general-graph');
|
|
const graphName = 'myUnittestGraph';
|
|
const db = require('internal').db;
|
|
let graph;
|
|
let edgeKey;
|
|
|
|
const ruleName = 'optimize-traversals';
|
|
// various choices to control the optimizer:
|
|
const paramEnabled = { optimizer: { rules: [ '-all', '+' + ruleName ] } };
|
|
const paramDisabled = { optimizer: { rules: [ '+all', '-' + ruleName ] } };
|
|
|
|
const cleanup = () => {
|
|
try {
|
|
graphModule._drop(graphName, true);
|
|
} catch (x) {
|
|
}
|
|
};
|
|
|
|
const assertRuleIsUsed = (query) => {
|
|
// assert the rule is not used when it's disabled
|
|
const planDisabled = AQL_EXPLAIN(query, { }, paramDisabled);
|
|
expect(planDisabled.plan.rules.indexOf(ruleName)).to.equal(-1, query);
|
|
|
|
// assert the rule is used when it's enabled
|
|
const planEnabled = AQL_EXPLAIN(query, { }, paramEnabled);
|
|
expect(planEnabled.plan.rules.indexOf(ruleName)).to.not.equal(-1, query);
|
|
};
|
|
|
|
const assertResultsAreUnchanged = (query) => {
|
|
// assert the rule is not used when it's disabled.
|
|
const planDisabled = AQL_EXPLAIN(query, { }, paramDisabled);
|
|
expect(planDisabled.plan.rules.indexOf(ruleName)).to.equal(-1, query);
|
|
|
|
const resultDisabled = AQL_EXECUTE(query, { }, paramDisabled).json;
|
|
const resultEnabled = AQL_EXECUTE(query, { }, paramEnabled).json;
|
|
|
|
expect(isEqual(resultDisabled, resultEnabled)).to.equal(true, query);
|
|
|
|
const opts = { allPlans: true, verbosePlans: true, optimizer: { rules: [ '-all', '+' + ruleName ] } };
|
|
|
|
const plans = AQL_EXPLAIN(query, {}, opts).plans;
|
|
plans.forEach(function (plan) {
|
|
const jsonResult = AQL_EXECUTEJSON(plan, { optimizer: { rules: [ '-all' ] } }).json;
|
|
expect(jsonResult).to.deep.equal(resultDisabled, query);
|
|
});
|
|
};
|
|
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
// / @brief test suite
|
|
// //////////////////////////////////////////////////////////////////////////////
|
|
|
|
describe('Rule optimize-traversals', () => {
|
|
// / @brief set up
|
|
before(() => {
|
|
cleanup();
|
|
graph = graphModule._create(graphName, [
|
|
graphModule._relation('edges', 'circles', 'circles')]);
|
|
|
|
// Add circle circles
|
|
graph.circles.save({'_key': 'A', 'label': '1'});
|
|
graph.circles.save({'_key': 'B', 'label': '2'});
|
|
graph.circles.save({'_key': 'C', 'label': '3'});
|
|
graph.circles.save({'_key': 'D', 'label': '4'});
|
|
graph.circles.save({'_key': 'E', 'label': '5'});
|
|
graph.circles.save({'_key': 'F', 'label': '6'});
|
|
graph.circles.save({'_key': 'G', 'label': '7'});
|
|
|
|
// Add relevant edges
|
|
edgeKey = graph.edges.save('circles/A', 'circles/C', {theFalse: false, theTruth: true, 'label': 'foo'})._key;
|
|
graph.edges.save('circles/A', 'circles/B', {theFalse: false, theTruth: true, 'label': 'bar'});
|
|
graph.edges.save('circles/B', 'circles/D', {theFalse: false, theTruth: true, 'label': 'blarg'});
|
|
graph.edges.save('circles/B', 'circles/E', {theFalse: false, theTruth: true, 'label': 'blub'});
|
|
graph.edges.save('circles/C', 'circles/F', {theFalse: false, theTruth: true, 'label': 'schubi'});
|
|
graph.edges.save('circles/C', 'circles/G', {theFalse: false, theTruth: true, 'label': 'doo'});
|
|
});
|
|
|
|
// / @brief tear down
|
|
after(cleanup);
|
|
|
|
it('should remove unused variables', () => {
|
|
const queries = [
|
|
[ `FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}' RETURN 1`, true, [ true, false, false ] ],
|
|
[ `FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}' RETURN [v, e]`, true, [ true, true, false ] ],
|
|
[ `FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}' RETURN [v, p]`, true, [ true, false, true ] ],
|
|
[ `FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}' RETURN [e, p]`, false, [ true, true, true ] ],
|
|
[ `FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}' RETURN [v]`, true, [ true, false, false ] ],
|
|
[ `FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}' RETURN [e]`, true, [ true, true, false ] ],
|
|
[ `FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}' RETURN [p]`, true, [ true, false, true ] ],
|
|
[ `FOR v, e IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}' RETURN [v, e]`, false, [ true, true, false ] ],
|
|
[ `FOR v, e IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}' RETURN [v]`, true, [ true, false, false ] ],
|
|
[ `FOR v IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}' RETURN [v]`, false, [ true, false, false ] ]
|
|
];
|
|
|
|
queries.forEach((query) => {
|
|
const result = AQL_EXPLAIN(query[0], { }, paramEnabled);
|
|
expect(result.plan.rules.indexOf(ruleName) !== -1).to.equal(query[1], query);
|
|
|
|
// check that variables were correctly optimized away
|
|
let found = false;
|
|
result.plan.nodes.forEach(function (thisNode) {
|
|
if (thisNode.type === 'TraversalNode') {
|
|
expect(thisNode.hasOwnProperty('vertexOutVariable')).to.equal(query[2][0]);
|
|
expect(thisNode.hasOwnProperty('edgeOutVariable')).to.equal(query[2][1]);
|
|
expect(thisNode.hasOwnProperty('pathOutVariable')).to.equal(query[2][2]);
|
|
found = true;
|
|
}
|
|
});
|
|
expect(found).to.be.true;
|
|
});
|
|
});
|
|
|
|
it('should not take effect in these queries', () => {
|
|
const queries = [
|
|
`FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}'
|
|
LET localScopeVar = NOOPT(true) FILTER p.edges[0].theTruth == localScopeVar
|
|
RETURN {v:v,e:e,p:p}`,
|
|
`FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}'
|
|
FILTER p.edges[-1].theTruth == true
|
|
RETURN {v:v,e:e,p:p}`,
|
|
`FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}'
|
|
FILTER p.edges[*].theTruth == true or p.edges[1].label == 'bar'
|
|
RETURN {v:v,e:e,p:p}`,
|
|
`FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}'
|
|
FILTER p.edges[RAND()].theFalse == false
|
|
RETURN {v:v,e:e,p:p}`,
|
|
`FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}'
|
|
FILTER p.edges[p.edges.length - 1].theFalse == false
|
|
RETURN {v:v,e:e,p:p}`,
|
|
`FOR v, e, p IN 2 OUTBOUND 'circles/A' GRAPH '${graphName}'
|
|
FILTER CONCAT(p.edges[0]._key, '') == " + edgeKey + " SORT v._key
|
|
RETURN {v:v,e:e,p:p}`,
|
|
`FOR v, e, p IN 2 OUTBOUND 'circles/A' GRAPH '${graphName}'
|
|
FILTER NOOPT(CONCAT(p.edges[0]._key, '')) == " + edgeKey + " SORT v._key
|
|
RETURN {v:v,e:e,p:p}`,
|
|
`FOR snippet IN ['a', 'b']
|
|
FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}'
|
|
FILTER p.edges[1].label == CONCAT(snippet, 'ar')
|
|
RETURN {v,e,p}`
|
|
];
|
|
|
|
queries.forEach(function (query) {
|
|
const result = AQL_EXPLAIN(query, { }, paramEnabled);
|
|
expect(result.plan.rules.indexOf(ruleName)).to.equal(-1, query);
|
|
});
|
|
});
|
|
|
|
it('should take effect in these queries', () => {
|
|
const queries = [
|
|
`FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}'
|
|
FILTER p.edges[0].theTruth == true
|
|
RETURN {v,e,p}`,
|
|
`FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}'
|
|
FILTER p.edges[1].theTruth == true
|
|
RETURN {v,e,p}`,
|
|
`FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}'
|
|
FILTER p.edges[2].theTruth == true
|
|
RETURN {v,e,p}`,
|
|
`FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}'
|
|
FILTER p.edges[2].theTruth == true AND p.edges[1].label == 'bar'
|
|
RETURN {v,e,p}`,
|
|
`FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}'
|
|
FILTER p.edges[0].theTruth == true FILTER p.edges[1].label == 'bar'
|
|
RETURN {v,e,p}`,
|
|
`FOR snippet IN ['a', 'b']
|
|
LET test = CONCAT(snippet, 'ar')
|
|
FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}'
|
|
FILTER p.edges[1].label == test
|
|
RETURN {v,e,p}`
|
|
];
|
|
queries.forEach(function (query) {
|
|
const result = AQL_EXPLAIN(query, { }, paramEnabled);
|
|
expect(result.plan.rules.indexOf(ruleName)).to.not.equal(-1, query);
|
|
result.plan.nodes.forEach((thisNode) => {
|
|
if (thisNode.type === 'TraversalNode') {
|
|
expect(thisNode.hasOwnProperty('condition')).to.equal(true, query);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should not modify the result', () => {
|
|
var queries = [
|
|
`FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}'
|
|
FILTER p.edges[0].label == 'foo'
|
|
RETURN {v,e,p}`,
|
|
`FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}'
|
|
FILTER p.edges[1].label == 'foo'
|
|
RETURN {v,e,p}`,
|
|
`FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}'
|
|
FILTER p.edges[0].theTruth == true FILTER p.edges[0].label == 'foo'
|
|
RETURN {v,e,p}`,
|
|
`FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}'
|
|
FILTER p.edges[0].theTruth == true FILTER p.edges[1].label == 'foo'
|
|
RETURN {v,e,p}`,
|
|
`FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}'
|
|
FILTER p.edges[0].theTruth == true FILTER p.edges[0].label == 'foo'
|
|
RETURN {v,e,p}`,
|
|
`FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}'
|
|
FILTER p.edges[1].theTruth == true FILTER p.edges[1].label == 'foo'
|
|
RETURN {v,e,p}`
|
|
];
|
|
|
|
queries.forEach(query => assertRuleIsUsed(query));
|
|
queries.forEach(query => assertResultsAreUnchanged(query));
|
|
});
|
|
|
|
it('should prune when using functions', () => {
|
|
let queries = [
|
|
`WITH circles FOR v, e, p IN 2 OUTBOUND @start @@ecol
|
|
FILTER p.edges[0]._key == CONCAT(@edgeKey, '')
|
|
SORT v._key RETURN v._key`,
|
|
`WITH circles FOR v, e, p IN 2 OUTBOUND @start @@ecol
|
|
FILTER p.edges[0]._key == @edgeKey
|
|
SORT v._key RETURN v._key`,
|
|
`WITH circles FOR v, e, p IN 2 OUTBOUND @start @@ecol
|
|
FILTER CONCAT(p.edges[0]._key, '') == @edgeKey
|
|
SORT v._key RETURN v._key`,
|
|
`WITH circles FOR v, e, p IN 2 OUTBOUND @start @@ecol
|
|
FILTER NOOPT(CONCAT(p.edges[0]._key, '')) == @edgeKey
|
|
SORT v._key RETURN v._key`
|
|
];
|
|
const bindVars = {
|
|
start: 'circles/A',
|
|
'@ecol': 'edges',
|
|
edgeKey: edgeKey
|
|
};
|
|
queries.forEach(function (q) {
|
|
let res = AQL_EXECUTE(q, bindVars, paramDisabled).json;
|
|
expect(res.length).to.equal(2, 'query: ' + q);
|
|
expect(res[0]).to.equal('F', 'query: ' + q);
|
|
expect(res[1]).to.equal('G', 'query: ' + q);
|
|
|
|
res = AQL_EXECUTE(q, bindVars, paramEnabled).json;
|
|
|
|
expect(res.length).to.equal(2, 'query (enabled): ' + q);
|
|
expect(res[0]).to.equal('F', 'query (enabled): ' + q);
|
|
expect(res[1]).to.equal('G', 'query (enabled): ' + q);
|
|
});
|
|
});
|
|
|
|
it('should short-circuit conditions that cannot be fulfilled', () => {
|
|
let queries = [
|
|
`FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}' FILTER p.edges[7].label == 'foo' RETURN {v,e,p}`,
|
|
// indexed access starts with 0 - this is also forbidden since it will look for the 6th!
|
|
`FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH '${graphName}' FILTER p.edges[5].label == 'foo' RETURN {v,e,p}`
|
|
// "FOR v, e, p IN 1..5 OUTBOUND 'circles/A' GRAPH 'myGraph' FILTER p.edges[1].label == 'foo' AND p.edges[1].label == 'bar' return {v:v,e:e,p:p}"
|
|
];
|
|
|
|
queries.forEach(function (query) {
|
|
let result = AQL_EXPLAIN(query, { }, paramEnabled);
|
|
let simplePlan = helper.getCompactPlan(result);
|
|
expect(result.plan.rules.indexOf(ruleName)).to.not.equal(-1, query);
|
|
expect(simplePlan[2].type).to.equal('NoResultsNode');
|
|
});
|
|
});
|
|
|
|
describe('using condition vars', () => {
|
|
it('taking invalid start form subquery', () => {
|
|
const query = `
|
|
LET data = (FOR i IN 1..1 RETURN i)
|
|
FOR v, e, p IN 1..10 OUTBOUND data GRAPH '${graphName}'
|
|
FILTER p.vertices[0]._id == '123'
|
|
FILTER p.vertices[1]._id != null
|
|
FILTER p.edges[0]._id IN data[*].foo.bar
|
|
RETURN 1`;
|
|
|
|
const result = AQL_EXPLAIN(query);
|
|
expect(result.plan.rules.indexOf(ruleName)).to.not.equal(-1, query);
|
|
expect(AQL_EXECUTE(query).json.length).to.equal(0);
|
|
});
|
|
|
|
it('filtering on a subquery', () => {
|
|
const query = `
|
|
LET data = (FOR i IN 1..1 RETURN i)
|
|
FOR v, e, p IN 1..10 OUTBOUND 'circles/A' GRAPH '${graphName}'
|
|
FILTER p.vertices[0]._id == '123'
|
|
FILTER p.vertices[1]._id != null
|
|
FILTER p.edges[0]._id IN data[*].foo.bar
|
|
RETURN 1`;
|
|
|
|
const result = AQL_EXPLAIN(query);
|
|
expect(result.plan.rules.indexOf(ruleName)).to.not.equal(-1, query);
|
|
expect(AQL_EXECUTE(query).json.length).to.equal(0);
|
|
});
|
|
|
|
it('filtering on a subquery 2', () => {
|
|
const query = `
|
|
LET data = (FOR i IN 1..1 RETURN i)
|
|
FOR v, e, p IN 1..10 OUTBOUND 'circles/A' GRAPH '${graphName}'
|
|
FILTER p.vertices[0]._id == '123'
|
|
FILTER p.vertices[1]._id != null
|
|
FILTER p.edges[0]._id IN data[*].foo.bar
|
|
FILTER p.edges[1]._key IN data[*].bar.baz._id
|
|
RETURN 1`;
|
|
|
|
const result = AQL_EXPLAIN(query);
|
|
expect(result.plan.rules.indexOf(ruleName)).to.not.equal(-1, query);
|
|
expect(AQL_EXECUTE(query).json.length).to.equal(0);
|
|
});
|
|
});
|
|
|
|
describe('filtering on own output', () => {
|
|
it('should filter with v on path', () => {
|
|
let query = `
|
|
FOR v,e,p IN 2 OUTBOUND @start GRAPH @graph
|
|
FILTER v.bar == p.edges[0].foo
|
|
RETURN {v,e,p}
|
|
`;
|
|
const bindVars = {
|
|
start: 'circles/A',
|
|
graph: graphName
|
|
};
|
|
|
|
let plan = AQL_EXPLAIN(query, bindVars, paramEnabled);
|
|
expect(plan.plan.rules.indexOf(ruleName)).to.equal(-1, query);
|
|
|
|
let result = AQL_EXECUTE(query, bindVars).json;
|
|
expect(result.length).to.equal(4);
|
|
|
|
query = `
|
|
FOR v,e,p IN 2 OUTBOUND @start GRAPH @graph
|
|
FILTER p.edges[0].foo == v.bar
|
|
RETURN {v,e,p}
|
|
`;
|
|
|
|
plan = AQL_EXPLAIN(query, bindVars, paramEnabled);
|
|
expect(plan.plan.rules.indexOf(ruleName)).to.equal(-1, query);
|
|
|
|
result = AQL_EXECUTE(query, bindVars).json;
|
|
expect(result.length).to.equal(4);
|
|
});
|
|
|
|
it('should filter with e on path', () => {
|
|
let query = `
|
|
FOR v,e,p IN 2 OUTBOUND @start GRAPH @graph
|
|
FILTER e.bar == p.edges[0].foo
|
|
RETURN {v,e,p}
|
|
`;
|
|
const bindVars = {
|
|
start: 'circles/A',
|
|
graph: graphName
|
|
};
|
|
let plan = AQL_EXPLAIN(query, bindVars, paramEnabled);
|
|
expect(plan.plan.rules.indexOf(ruleName)).to.equal(-1, query);
|
|
|
|
let result = AQL_EXECUTE(query, bindVars).json;
|
|
expect(result.length).to.equal(4);
|
|
|
|
query = `
|
|
FOR v,e,p IN 2 OUTBOUND @start GRAPH @graph
|
|
FILTER p.edges[0].foo == e.bar
|
|
RETURN {v,e,p}
|
|
`;
|
|
|
|
plan = AQL_EXPLAIN(query, bindVars, paramEnabled);
|
|
expect(plan.plan.rules.indexOf(ruleName)).to.equal(-1, query);
|
|
|
|
result = AQL_EXECUTE(query, bindVars).json;
|
|
expect(result.length).to.equal(4);
|
|
});
|
|
|
|
it('should filter with path.edges on path.edges', () => {
|
|
let query = `
|
|
FOR v,e,p IN 2 OUTBOUND @start GRAPH @graph
|
|
FILTER p.edges[1].foo == p.edges[0].foo
|
|
RETURN {v,e,p}
|
|
`;
|
|
const bindVars = {
|
|
start: 'circles/A',
|
|
graph: graphName
|
|
};
|
|
|
|
let plan = AQL_EXPLAIN(query, bindVars, paramEnabled);
|
|
expect(plan.plan.rules.indexOf(ruleName)).to.equal(-1, query);
|
|
|
|
let result = AQL_EXECUTE(query, bindVars).json;
|
|
expect(result.length).to.equal(4);
|
|
|
|
query = `
|
|
FOR v,e,p IN 2 OUTBOUND @start GRAPH @graph
|
|
FILTER p.edges[0].foo == p.edges[1].foo
|
|
RETURN {v,e,p}
|
|
`;
|
|
|
|
result = AQL_EXECUTE(query, bindVars).json;
|
|
expect(result.length).to.equal(4);
|
|
});
|
|
|
|
it('should filter with path.vertices on path.vertices', () => {
|
|
let query = `
|
|
FOR v,e,p IN 2 OUTBOUND @start GRAPH @graph
|
|
FILTER p.vertices[1].foo == p.vertices[0].foo
|
|
RETURN {v,e,p}
|
|
`;
|
|
const bindVars = {
|
|
start: 'circles/A',
|
|
graph: graphName
|
|
};
|
|
let plan = AQL_EXPLAIN(query, bindVars, paramEnabled);
|
|
expect(plan.plan.rules.indexOf(ruleName)).to.equal(-1, query);
|
|
|
|
let result = AQL_EXECUTE(query, bindVars).json;
|
|
expect(result.length).to.equal(4);
|
|
|
|
query = `
|
|
FOR v,e,p IN 2 OUTBOUND @start GRAPH @graph
|
|
FILTER p.vertices[0].foo == p.vertices[1].foo
|
|
RETURN {v,e,p}
|
|
`;
|
|
|
|
plan = AQL_EXPLAIN(query, bindVars, paramEnabled);
|
|
expect(plan.plan.rules.indexOf(ruleName)).to.equal(-1, query);
|
|
|
|
result = AQL_EXECUTE(query, bindVars).json;
|
|
expect(result.length).to.equal(4);
|
|
});
|
|
|
|
it('should filter with path.vertices on path.edges', () => {
|
|
let query = `
|
|
FOR v,e,p IN 2 OUTBOUND @start GRAPH @graph
|
|
FILTER p.vertices[1].foo == p.edges[0].foo
|
|
RETURN {v,e,p}
|
|
`;
|
|
const bindVars = {
|
|
start: 'circles/A',
|
|
graph: graphName
|
|
};
|
|
|
|
let plan = AQL_EXPLAIN(query, bindVars, paramEnabled);
|
|
expect(plan.plan.rules.indexOf(ruleName)).to.equal(-1, query);
|
|
|
|
let result = AQL_EXECUTE(query, bindVars).json;
|
|
expect(result.length).to.equal(4);
|
|
|
|
query = `
|
|
FOR v,e,p IN 2 OUTBOUND @start GRAPH @graph
|
|
FILTER p.edges[0].foo == p.vertices[1].foo
|
|
RETURN {v,e,p}
|
|
`;
|
|
|
|
plan = AQL_EXPLAIN(query, bindVars, paramEnabled);
|
|
expect(plan.plan.rules.indexOf(ruleName)).to.equal(-1, query);
|
|
|
|
result = AQL_EXECUTE(query, bindVars).json;
|
|
expect(result.length).to.equal(4);
|
|
});
|
|
});
|
|
|
|
describe('various filter optimizations', () => {
|
|
const multiplyArray = (left, right) => {
|
|
// Only works if both are arrays
|
|
let res = [];
|
|
for (let l of left) {
|
|
for (let r of right) {
|
|
res.push(l + r);
|
|
}
|
|
}
|
|
return res;
|
|
};
|
|
|
|
const multiplyArrays = (left, middle, right) => {
|
|
// Only works if both are arrays
|
|
let res = [];
|
|
for (let l of left) {
|
|
for (let m of middle) {
|
|
for (let r of right) {
|
|
res.push(l + m + r);
|
|
}
|
|
}
|
|
}
|
|
return res;
|
|
};
|
|
|
|
const symetricOperators = [
|
|
` == `, ` < `, ` <= `, ` != `
|
|
];
|
|
const asymetricOperators = [
|
|
` IN `, ` NOT IN `
|
|
];
|
|
|
|
const modifiers = [
|
|
` ALL`, ` NONE`
|
|
];
|
|
const arrayCmpStart = [
|
|
`p.edges[*]`,
|
|
`p.vertices[*]`
|
|
];
|
|
const singleCompStart = [
|
|
`p.edges[0]`,
|
|
`p.vertices[1]`
|
|
];
|
|
|
|
const outputStart = [
|
|
`v`, `e`, `p`
|
|
];
|
|
|
|
const valuePostFixes = [
|
|
``, `.foo`, `.foo[*]`, `.foo.bar`, `.foo[0]`
|
|
];
|
|
|
|
const constValues = [
|
|
`1`,
|
|
`'test'`,
|
|
`null`,
|
|
`2 + 1`,
|
|
`['foo', 'bar']`,
|
|
`{foo: 'bar'}`
|
|
];
|
|
|
|
const functValues = [
|
|
`NOOPT(V8(3+2))`,
|
|
`APPEND(['foo'], ['bar'], false)`
|
|
];
|
|
const queryStart = `FOR v,e,p IN 1..3 OUTBOUND 'circles/A' GRAPH '${graphName}' FILTER `;
|
|
const queryEnd = ` RETURN {v,e,p}`;
|
|
|
|
const symOperatorsWithModifiers = multiplyArray(modifiers, symetricOperators);
|
|
const noOptSymOperators = multiplyArray([` ANY`], symetricOperators);
|
|
|
|
const checkDoesOptimize = (conditions, shouldOptimize) => {
|
|
for (let c of conditions) {
|
|
const q = queryStart + c + queryEnd;
|
|
try {
|
|
const expl = AQL_EXPLAIN(q, { });
|
|
if (shouldOptimize) {
|
|
expect(expl.plan.rules.indexOf(ruleName)).to.not.equal(-1, c);
|
|
} else {
|
|
expect(expl.plan.rules.indexOf(ruleName)).to.equal(-1, c);
|
|
}
|
|
const result = db._query(q);
|
|
// This is just to validate that the server does not crash!
|
|
expect(result.count()).to.be.at.least(0, c);
|
|
} catch (e) {
|
|
// if sth throws an error we should see it
|
|
expect(e.errorMessage).to.equal(undefined, c);
|
|
expect(e.errorNum).to.equal(undefined, c);
|
|
}
|
|
}
|
|
};
|
|
|
|
const arrayStarts = multiplyArray(arrayCmpStart, valuePostFixes);
|
|
const singleStarts = multiplyArray(singleCompStart, valuePostFixes);
|
|
|
|
describe('should optimize', () => {
|
|
it('symetric operators for point access vs constants', () => {
|
|
const conditions = multiplyArrays(singleStarts, symetricOperators, constValues)
|
|
.concat(multiplyArrays(constValues, symetricOperators, singleStarts));
|
|
checkDoesOptimize(conditions, true);
|
|
});
|
|
|
|
it('array modifiers with path array access left', () => {
|
|
const conditions = multiplyArrays(arrayStarts, symOperatorsWithModifiers, constValues);
|
|
checkDoesOptimize(conditions, true);
|
|
});
|
|
|
|
it('array modifiers with point access left', () => {
|
|
const conditions = multiplyArrays(constValues, noOptSymOperators, singleStarts);
|
|
checkDoesOptimize(conditions, true);
|
|
});
|
|
|
|
it('asymetric operators with path array access left (NOT ANY)', () => {
|
|
const ops = multiplyArray(modifiers.concat([``]), asymetricOperators);
|
|
const conditions = multiplyArrays(arrayStarts, ops, constValues);
|
|
checkDoesOptimize(conditions, true);
|
|
});
|
|
});
|
|
|
|
describe('should not optimize', () => {
|
|
it('ANY array modifiers with path array access left', () => {
|
|
const conditions = multiplyArrays(arrayStarts, noOptSymOperators, constValues);
|
|
checkDoesOptimize(conditions, false);
|
|
});
|
|
|
|
it('array modifiers with path array access right', () => {
|
|
const conditions = multiplyArrays(constValues, noOptSymOperators, arrayStarts);
|
|
checkDoesOptimize(conditions, false);
|
|
});
|
|
|
|
it('asymetric operators with path array access right', () => {
|
|
const ops = multiplyArray(([``, ` ANY`].concat(modifiers)), asymetricOperators);
|
|
const conditions = multiplyArrays(constValues, ops, arrayStarts);
|
|
checkDoesOptimize(conditions, false);
|
|
});
|
|
|
|
it('asymetric operators with ANY and path array access left', () => {
|
|
const ops = multiplyArray([` ANY`], asymetricOperators);
|
|
const conditions = multiplyArrays(arrayStarts, ops, constValues);
|
|
checkDoesOptimize(conditions, false);
|
|
});
|
|
|
|
it('functions symetric operators with array access', () => {
|
|
let ops = multiplyArray([``, ` ANY`].concat(modifiers), symetricOperators);
|
|
let conditions = multiplyArrays(arrayStarts, ops, functValues);
|
|
checkDoesOptimize(conditions, false);
|
|
conditions = multiplyArrays(functValues, ops, arrayStarts);
|
|
checkDoesOptimize(conditions, false);
|
|
});
|
|
|
|
it('functions asymetric operators with array access', () => {
|
|
let ops = multiplyArray([``, ` ANY`].concat(modifiers), asymetricOperators);
|
|
let conditions = multiplyArrays(arrayStarts, ops, functValues);
|
|
checkDoesOptimize(conditions, false);
|
|
conditions = multiplyArrays(functValues, ops, arrayStarts);
|
|
checkDoesOptimize(conditions, false);
|
|
});
|
|
|
|
it('functions symetric operators with point access', () => {
|
|
let ops = multiplyArray([``, ` ANY`].concat(modifiers), symetricOperators);
|
|
let conditions = multiplyArrays(singleStarts, ops, functValues);
|
|
checkDoesOptimize(conditions, false);
|
|
conditions = multiplyArrays(functValues, ops, singleStarts);
|
|
checkDoesOptimize(conditions, false);
|
|
});
|
|
|
|
it('functions asymetric operators with point access', () => {
|
|
let ops = multiplyArray([``, ` ANY`].concat(modifiers), asymetricOperators);
|
|
let conditions = multiplyArrays(singleStarts, ops, functValues);
|
|
checkDoesOptimize(conditions, false);
|
|
conditions = multiplyArrays(functValues, ops, singleStarts);
|
|
checkDoesOptimize(conditions, false);
|
|
});
|
|
|
|
it('other access referencing traversal output', () => {
|
|
// Build all combinations of v,e,p
|
|
const starters = multiplyArray(outputStart.concat(arrayCmpStart).concat(singleCompStart), valuePostFixes);
|
|
const conditions = multiplyArrays(starters, symetricOperators, starters);
|
|
checkDoesOptimize(conditions, false);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('regression tests', () => {
|
|
before(cleanup);
|
|
after(cleanup);
|
|
|
|
// https://github.com/arangodb/release-3.4/issues/91
|
|
// Regression test: With 'optimize-traversals' enabled, the filter
|
|
// p.vertices[* FILTER CURRENT._id != 'V/1'].label ALL == true
|
|
// was effectively changed to
|
|
// p.vertices[*].label ALL == true
|
|
// . That is, inline expressions were ignored.
|
|
it('inline expressions should not be changed', () => {
|
|
// create data
|
|
graph = graphModule._create(graphName, [
|
|
graphModule._relation('E', 'V', 'V')]);
|
|
|
|
graph.V.save({_key: '1', label: false});
|
|
graph.V.save({_key: '2', label: true});
|
|
graph.V.save({_key: '3', label: false});
|
|
|
|
graph.E.save('V/1', 'V/2', {});
|
|
graph.E.save('V/1', 'V/3', {});
|
|
|
|
const query = `
|
|
FOR v, e, p IN 1..10 OUTBOUND 'V/1' GRAPH '${graphName}'
|
|
FILTER p.vertices[* FILTER CURRENT._id != 'V/1'].label ALL == true
|
|
RETURN p.vertices
|
|
`;
|
|
|
|
assertResultsAreUnchanged(query);
|
|
});
|
|
});
|
|
});
|