1
0
Fork 0

fix dump&restore of smart graphs (#5065)

* Added tests for dump-restore of SmartGraphs

* Arangosh will now expose isSmart and the smartGraphAttribute on properties

* RestReplicationHandler will now ignore smart-graph collections unless you execute it with force

* Added changelog

* Reactivated original mmfiles dump/restore test

* Skip hidden smart graph collections in arangodump

* Do not dump shadowCollections metadata of smart edge collections

* Cluster optimizer rules for soerted gather nodes now handle virtual edge collections correctly

* Added a dump/restore tests for smartgraphs in rocksdb as well

* Deactivated checks for writesExecuted statistics in dump/restore tests for smartgraphs mmfiles

* Really exclude shadowCollections

* Reduced loglevel

* Added tests

* Don't change single-server behaviour

* Fix tests for omitted shadowCollections and hidden collections

* Activated statistics in MMFIles dump test again and included isEnterprise in rocksdb dump test

* A modification node can now disableStatistics, which means it does not contribute to query->extras() this is only relevant in SmartGraph case so far.

* Added a test to dump&restore satellite collections

* Bugfix: restore satellite collections properly

* Added regression test for internal issue #2237

* Fix bug #2237

* Updated CHANGELOG

* Copied dump/restore tests to rockdsb

* Removed enterprise test

* Added inline comment for smart-edge collections in optimizer rules

* Removed duplicate CHANGELOG entry

* Simplified removal of shadowCollections
This commit is contained in:
Michael Hackstein 2018-04-10 17:21:03 +02:00 committed by GitHub
parent c07a706948
commit 5decb66d01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 831 additions and 27 deletions

View File

@ -1,11 +1,24 @@
v3.3.6.1 (XXXX-XX-XX)
---------------------
* Fixed internal issue #2237: AQL queries on collections with replicationFactor:
"satellite" crashed arangod in single server mode
* Fixed restore of satellite collections: replicationFactor was set to 1 during
restore
* Fixed dump and restore of smart graphs:
a) The dump will not include the hidden shadow collections anymore, they were dumped
accidentially and only contain duplicated data.
b) Restore will now ignore hidden shadow collections as all data is contained
in the smart-edge collection. You can manually include these collections from an
old dump (3.3.5 or earlier) by using `--force`.
c) Restore of a smart-graph will now create smart collections properly instead
of getting into `TIMEOUT_IN_CLUSTER_OPERATION`
* fixed issue in AQL query optimizer rule "restrict-to-single-shard", which
may have sent documents to a wrong shard in AQL INSERT queries that specified
the value for `_key` using an expression (and not a constant value)
* fix restoring of smart graph edge collections (may have run into timeout before)
* added /_admin/status for debugging

View File

@ -56,7 +56,7 @@ EnumerateCollectionBlock::EnumerateCollectionBlock(
int EnumerateCollectionBlock::initialize() {
DEBUG_BEGIN_BLOCK();
if (_collection->isSatellite()) {
if (ServerState::instance()->isRunningInCluster() && _collection->isSatellite()) {
auto logicalCollection = _collection->getCollection();
auto cid = logicalCollection->planId();
auto dbName = logicalCollection->dbName();

View File

@ -46,7 +46,8 @@ ModificationBlock::ModificationBlock(ExecutionEngine* engine,
_outRegNew(ExecutionNode::MaxRegisterId),
_collection(ep->_collection),
_isDBServer(false),
_usesDefaultSharding(true) {
_usesDefaultSharding(true),
_countStats(ep->countStats()) {
_trx->pinData(_collection->cid());
@ -183,13 +184,17 @@ void ModificationBlock::handleResult(int code, bool ignoreErrors,
std::string const* errorMessage) {
if (code == TRI_ERROR_NO_ERROR) {
// update the success counter
++_engine->_stats.writesExecuted;
if (_countStats) {
++_engine->_stats.writesExecuted;
}
return;
}
if (ignoreErrors) {
// update the ignored counter
++_engine->_stats.writesIgnored;
if (_countStats) {
++_engine->_stats.writesIgnored;
}
return;
}
@ -209,18 +214,24 @@ void ModificationBlock::handleBabyResult(std::unordered_map<int, size_t> const&
if (errorCounter.empty()) {
// update the success counter
// All successful.
_engine->_stats.writesExecuted += numBabies;
if (_countStats) {
_engine->_stats.writesExecuted += numBabies;
}
return;
}
if (ignoreAllErrors) {
for (auto const& pair : errorCounter) {
// update the ignored counter
_engine->_stats.writesIgnored += pair.second;
if (_countStats) {
_engine->_stats.writesIgnored += pair.second;
}
numBabies -= pair.second;
}
// update the success counter
_engine->_stats.writesExecuted += numBabies;
if (_countStats) {
_engine->_stats.writesExecuted += numBabies;
}
return;
}
auto first = errorCounter.begin();
@ -229,10 +240,14 @@ void ModificationBlock::handleBabyResult(std::unordered_map<int, size_t> const&
if (errorCounter.size() == 1) {
// We only have Document not found. Fix statistics and ignore
// update the ignored counter
_engine->_stats.writesIgnored += first->second;
if (_countStats) {
_engine->_stats.writesIgnored += first->second;
}
numBabies -= first->second;
// update the success counter
_engine->_stats.writesExecuted += numBabies;
if (_countStats) {
_engine->_stats.writesExecuted += numBabies;
}
return;
}

View File

@ -73,6 +73,10 @@ class ModificationBlock : public ExecutionBlock {
/// @brief whether or not the collection uses the default sharding attributes
bool _usesDefaultSharding;
/// @brief whether this block contributes to statistics.
/// Will only be disabled in SmartGraphCase.
bool _countStats;
};
class RemoveBlock : public ModificationBlock {

View File

@ -42,7 +42,8 @@ ModificationNode::ModificationNode(ExecutionPlan* plan,
_outVariableOld(
Variable::varFromVPack(plan->getAst(), base, "outVariableOld", Optional)),
_outVariableNew(
Variable::varFromVPack(plan->getAst(), base, "outVariableNew", Optional)) {
Variable::varFromVPack(plan->getAst(), base, "outVariableNew", Optional)),
_countStats(base.get("countStats").getBool()) {
TRI_ASSERT(_vocbase != nullptr);
TRI_ASSERT(_collection != nullptr);
}
@ -55,6 +56,7 @@ void ModificationNode::toVelocyPackHelper(VPackBuilder& builder,
// Now put info about vocbase and cid in there
builder.add("database", VPackValue(_vocbase->name()));
builder.add("collection", VPackValue(_collection->getName()));
builder.add("countStats", VPackValue(_countStats));
// add out variables
if (_outVariableOld != nullptr) {
@ -66,6 +68,7 @@ void ModificationNode::toVelocyPackHelper(VPackBuilder& builder,
_outVariableNew->toVelocyPack(builder);
}
builder.add(VPackValue("modificationFlags"));
_options.toVelocyPack(builder);
}
@ -114,6 +117,9 @@ ExecutionNode* RemoveNode::clone(ExecutionPlan* plan, bool withDependencies,
auto c = new RemoveNode(plan, _id, _vocbase, _collection, _options,
inVariable, outVariableOld);
if (!_countStats) {
c->disableStatistics();
}
cloneHelper(c, withDependencies, withProperties);
@ -153,6 +159,9 @@ ExecutionNode* InsertNode::clone(ExecutionPlan* plan, bool withDependencies,
auto c = new InsertNode(plan, _id, _vocbase, _collection, _options,
inVariable, outVariableNew);
if (!_countStats) {
c->disableStatistics();
}
cloneHelper(c, withDependencies, withProperties);
@ -209,6 +218,9 @@ ExecutionNode* UpdateNode::clone(ExecutionPlan* plan, bool withDependencies,
auto c =
new UpdateNode(plan, _id, _vocbase, _collection, _options, inDocVariable,
inKeyVariable, outVariableOld, outVariableNew);
if (!_countStats) {
c->disableStatistics();
}
cloneHelper(c, withDependencies, withProperties);
@ -266,6 +278,9 @@ ExecutionNode* ReplaceNode::clone(ExecutionPlan* plan, bool withDependencies,
auto c =
new ReplaceNode(plan, _id, _vocbase, _collection, _options, inDocVariable,
inKeyVariable, outVariableOld, outVariableNew);
if (!_countStats) {
c->disableStatistics();
}
cloneHelper(c, withDependencies, withProperties);
@ -319,6 +334,9 @@ ExecutionNode* UpsertNode::clone(ExecutionPlan* plan, bool withDependencies,
auto c = new UpsertNode(plan, _id, _vocbase, _collection, _options,
inDocVariable, insertVariable, updateVariable,
outVariableNew, _isReplace);
if (!_countStats) {
c->disableStatistics();
}
cloneHelper(c, withDependencies, withProperties);

View File

@ -55,7 +55,8 @@ class ModificationNode : public ExecutionNode {
_collection(collection),
_options(options),
_outVariableOld(outVariableOld),
_outVariableNew(outVariableNew) {
_outVariableNew(outVariableNew),
_countStats(true) {
TRI_ASSERT(_vocbase != nullptr);
TRI_ASSERT(_collection != nullptr);
}
@ -127,6 +128,12 @@ class ModificationNode : public ExecutionNode {
/// @brief whether or not the node is a data modification node
bool isModificationNode() const override { return true; }
/// @brief whether this node contributes to statistics. Only disabled in SmartGraph case
bool countStats() const { return _countStats; }
/// @brief Disable that this node is contributing to statistics. Only disabled in SmartGraph case
void disableStatistics() { _countStats = false; }
protected:
/// @brief _vocbase, the database
TRI_vocbase_t* _vocbase;
@ -142,6 +149,9 @@ class ModificationNode : public ExecutionNode {
/// @brief output variable ($NEW)
Variable const* _outVariableNew;
/// @brief whether this node contributes to statistics. Only disabled in SmartGraph case
bool _countStats;
};
/// @brief class RemoveNode

View File

@ -2916,7 +2916,9 @@ void arangodb::aql::scatterInClusterRule(Optimizer* opt,
plan->registerNode(gatherNode);
TRI_ASSERT(remoteNode);
gatherNode->addDependency(remoteNode);
if (!elements.empty() && gatherNode->collection()->numberOfShards() > 1) {
// On SmartEdge collections we have 0 shards and we need the elements
// to be injected here as well. So do not replace it with > 1
if (!elements.empty() && gatherNode->collection()->numberOfShards() != 1) {
gatherNode->setElements(elements);
}
@ -3443,7 +3445,9 @@ void arangodb::aql::distributeSortToClusterRule(
if (thisSortNode->_reinsertInCluster) {
plan->insertDependency(rn, inspectNode);
}
if (gatherNode->collection()->numberOfShards() > 1) {
// On SmartEdge collections we have 0 shards and we need the elements
// to be injected here as well. So do not replace it with > 1
if (gatherNode->collection()->numberOfShards() != 1) {
gatherNode->setElements(thisSortNode->getElements());
}
modified = true;

View File

@ -68,6 +68,20 @@ using namespace arangodb::rest;
uint64_t const RestReplicationHandler::_defaultChunkSize = 128 * 1024;
uint64_t const RestReplicationHandler::_maxChunkSize = 128 * 1024 * 1024;
static bool ignoreHiddenEnterpriseCollection(std::string const& name, bool force) {
#ifdef USE_ENTERPRISE
if (!force && name[0] == '_') {
if (strncmp(name.c_str(), "_local_", 7) == 0 ||
strncmp(name.c_str(), "_from_", 6) == 0 ||
strncmp(name.c_str(), "_to_", 4) == 0) {
LOG_TOPIC(WARN, arangodb::Logger::FIXME) << "Restore ignoring collection " << name << ". Will be created via SmartGraphs of a full dump. If you want to restore ONLY this collection use 'arangorestore --force'. However this is not recommended and you should instead restore the EdgeCollection of the SmartGraph instead.";
return true;
}
}
#endif
return false;
}
static Result restoreDataParser(char const* ptr, char const* pos,
std::string const& collectionName,
std::string& key,
@ -969,6 +983,10 @@ Result RestReplicationHandler::processRestoreCollectionCoordinator(
return Result(TRI_ERROR_HTTP_BAD_PARAMETER, "collection name is missing");
}
if (ignoreHiddenEnterpriseCollection(name, force)) {
return {TRI_ERROR_NO_ERROR};
}
if (arangodb::basics::VelocyPackHelper::getBooleanValue(parameters, "deleted",
false)) {
// we don't care about deleted collections
@ -1042,7 +1060,10 @@ Result RestReplicationHandler::processRestoreCollectionCoordinator(
// Replication Factor. Will be overwritten if not existent
VPackSlice const replFactorSlice = parameters.get("replicationFactor");
if (!replFactorSlice.isInteger()) {
bool isValidReplFactorSlice =
replFactorSlice.isInteger() ||
(replFactorSlice.isString() && replFactorSlice.isEqualString("satellite"));
if (!isValidReplFactorSlice) {
if (replicationFactor == 0) {
replicationFactor = 1;
}
@ -1057,6 +1078,11 @@ Result RestReplicationHandler::processRestoreCollectionCoordinator(
// system collection?
toMerge.add("isSystem", VPackValue(true));
}
// Always ignore `shadowCollections` they were accidentially dumped in arangodb versions
// earlier than 3.3.6
toMerge.add("shadowCollections", arangodb::basics::VelocyPackHelper::NullValue());
toMerge.close(); // TopLevel
VPackSlice const type = parameters.get("type");
@ -1068,7 +1094,7 @@ Result RestReplicationHandler::processRestoreCollectionCoordinator(
VPackSlice const sliceToMerge = toMerge.slice();
VPackBuilder mergedBuilder =
VPackCollection::merge(parameters, sliceToMerge, false);
VPackCollection::merge(parameters, sliceToMerge, false, true);
VPackSlice const merged = mergedBuilder.slice();
try {
@ -1107,6 +1133,21 @@ Result RestReplicationHandler::processRestoreCollectionCoordinator(
////////////////////////////////////////////////////////////////////////////////
Result RestReplicationHandler::processRestoreData(std::string const& colName) {
#ifdef USE_ENTERPRISE
{
bool force = false;
bool found = false;
std::string const& forceVal = _request->value("force", found);
if (found) {
force = StringUtils::boolean(forceVal);
}
if (ignoreHiddenEnterpriseCollection(colName, force)) {
return {TRI_ERROR_NO_ERROR};
}
}
#endif
grantTemporaryRights();
if (colName == "_users") {
@ -1476,6 +1517,7 @@ int RestReplicationHandler::processRestoreIndexes(VPackSlice const& collection,
return TRI_ERROR_HTTP_BAD_PARAMETER;
}
VPackSlice const parameters = collection.get("parameters");
if (!parameters.isObject()) {
@ -1613,6 +1655,10 @@ int RestReplicationHandler::processRestoreIndexesCoordinator(
return TRI_ERROR_HTTP_BAD_PARAMETER;
}
if (ignoreHiddenEnterpriseCollection(name, force)) {
return {TRI_ERROR_NO_ERROR};
}
if (arangodb::basics::VelocyPackHelper::getBooleanValue(parameters, "deleted",
false)) {
// we don't care about deleted collections

View File

@ -24,6 +24,7 @@
#include <iostream>
#include <velocypack/Collection.h>
#include <velocypack/Iterator.h>
#include <velocypack/velocypack-aliases.h>
@ -903,6 +904,10 @@ int DumpFeature::runClusterDump(std::string& errorMsg) {
continue;
}
if (isIgnoredHiddenEnterpriseCollection(name)) {
continue;
}
if (!_ignoreDistributeShardsLikeErrors) {
std::string prototypeCollection =
arangodb::basics::VelocyPackHelper::getStringValue(
@ -954,7 +959,21 @@ int DumpFeature::runClusterDump(std::string& errorMsg) {
beginEncryption(fd);
std::string const collectionInfo = collection.toJson();
VPackBuilder excludes;
{ // { parameters: { shadowCollections: null } }
excludes.add(VPackValue(VPackValueType::Object));
excludes.add("parameters", VPackValue(VPackValueType::Object));
excludes.add("shadowCollections", VPackSlice::nullSlice());
excludes.close();
excludes.close();
}
VPackBuilder collectionWithExcludedParametersBuilder
= VPackCollection::merge(collection, excludes.slice(), true, true);
std::string const collectionInfo =
collectionWithExcludedParametersBuilder.slice().toJson();
bool result =
writeData(fd, collectionInfo.c_str(), collectionInfo.size());
@ -1237,3 +1256,23 @@ void DumpFeature::endEncryption(int fd) {
}
#endif
}
bool DumpFeature::isIgnoredHiddenEnterpriseCollection(
std::string const& name) const {
#ifdef USE_ENTERPRISE
if (!_force && name[0] == '_') {
if (strncmp(name.c_str(), "_local_", 7) == 0 ||
strncmp(name.c_str(), "_from_", 6) == 0 ||
strncmp(name.c_str(), "_to_", 4) == 0) {
LOG_TOPIC(INFO, arangodb::Logger::FIXME)
<< "Dump ignoring collection " << name
<< ". Will be created via SmartGraphs of a full dump. If you want to "
"dump this collection anyway use 'arangodump --force'. "
"However this is not recommended and you should instead dump "
"the EdgeCollection of the SmartGraph instead.";
return true;
}
}
#endif
return false;
}

View File

@ -78,6 +78,8 @@ class DumpFeature final : public application_features::ApplicationFeature,
void beginEncryption(int fd);
void endEncryption(int fd);
bool isIgnoredHiddenEnterpriseCollection(std::string const &name) const;
private:
int* _result;
uint64_t _batchId;
@ -91,6 +93,7 @@ class DumpFeature final : public application_features::ApplicationFeature,
uint64_t _totalCollections;
uint64_t _totalWritten;
} _stats;
};
}

View File

@ -343,10 +343,12 @@ ArangoCollection.prototype.properties = function (properties) {
var attributes = {
'doCompact': true,
'journalSize': true,
'isSmart': false,
'isSystem': false,
'isVolatile': false,
'waitForSync': true,
'shardKeys': false,
'smartGraphAttribute': false,
'numberOfShards': false,
'keyOptions': false,
'indexBuckets': true,

View File

@ -28,8 +28,11 @@
/// @author Copyright 2012, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////
var internal = require("internal");
var jsunity = require("jsunity");
const fs = require('fs');
const internal = require("internal");
const jsunity = require("jsunity");
const isEnterprise = internal.isEnterprise();
const db = internal.db;
////////////////////////////////////////////////////////////////////////////////
/// @brief test suite
@ -37,7 +40,6 @@ var jsunity = require("jsunity");
function dumpTestSuite () {
'use strict';
var db = internal.db;
return {
@ -320,11 +322,300 @@ function dumpTestSuite () {
};
}
////////////////////////////////////////////////////////////////////////////////
/// @brief test suite for the enterprise mode
////////////////////////////////////////////////////////////////////////////////
function dumpTestEnterpriseSuite () {
const smartGraphName = "UnitTestDumpSmartGraph";
const edges = "UnitTestDumpSmartEdges";
const vertices = "UnitTestDumpSmartVertices";
const orphans = "UnitTestDumpSmartOrphans";
const satellite = "UnitTestDumpSatelliteCollection";
const gm = require("@arangodb/smart-graph");
const instanceInfo = JSON.parse(require('internal').env.INSTANCEINFO);
return {
////////////////////////////////////////////////////////////////////////////////
/// @brief set up
////////////////////////////////////////////////////////////////////////////////
setUp : function () {
},
////////////////////////////////////////////////////////////////////////////////
/// @brief tear down
////////////////////////////////////////////////////////////////////////////////
tearDown : function () {
},
testSatelliteCollections : function () {
let c = db._collection(satellite);
let p = c.properties();
assertEqual(2, c.type()); // Document
assertEqual(1, p.numberOfShards);
assertEqual("satellite", p.replicationFactor);
assertEqual(100, c.count());
},
testHiddenCollectionsOmitted : function () {
const dumpDir = fs.join(instanceInfo.rootDir, 'dump');
const smartEdgeCollectionPath = fs.join(dumpDir, `${edges}.structure.json`);
const localEdgeCollectionPath = fs.join(dumpDir, `_local_${edges}.structure.json`);
const fromEdgeCollectionPath = fs.join(dumpDir, `_from_${edges}.structure.json`);
const toEdgeCollectionPath = fs.join(dumpDir, `_to_${edges}.structure.json`);
assertTrue(fs.exists(smartEdgeCollectionPath), 'Smart edge collection missing in dump!');
assertFalse(fs.exists(localEdgeCollectionPath), '_local edge collection should not have been dumped!');
assertFalse(fs.exists(fromEdgeCollectionPath), '_from edge collection should not have been dumped!');
assertFalse(fs.exists(toEdgeCollectionPath), '_to edge collection should not have been dumped!');
},
testShadowCollectionsOmitted : function () {
const dumpDir = fs.join(instanceInfo.rootDir, 'dump');
const collStructure = JSON.parse(
fs.read(fs.join(dumpDir, `${edges}.structure.json`))
);
assertTrue(collStructure.hasOwnProperty('parameters'), collStructure);
const parameters = collStructure['parameters'];
assertFalse(parameters.hasOwnProperty('shadowCollections'),
`Property 'shadowCollections' should be hidden in collection ${edges}!`);
},
testVertices : function () {
let c = db._collection(vertices);
let p = c.properties();
assertEqual(2, c.type()); // Document
assertEqual(5, p.numberOfShards);
assertTrue(p.isSmart, p);
assertFalse(Object.hasOwnProperty(p, "distributeShardsLike"));
assertEqual(100, c.count());
assertEqual("value", p.smartGraphAttribute);
},
testVerticesAqlRead: function () {
let q1 = `FOR x IN ${vertices} SORT TO_NUMBER(x.value) RETURN x`;
let q2 = `FOR x IN ${vertices} FILTER x.value == "10" RETURN x.value`;
// This query can be optimized to a single shard. Make sure that is still correct
let q3 = `FOR x IN ${vertices} FILTER x._key == @key RETURN x.value`;
let res1 = db._query(q1).toArray();
assertEqual(100, res1.length);
for (let i = 0; i < 100; ++i) {
assertEqual(String(i), res1[i].value);
}
let res2 = db._query(q2).toArray();
assertEqual(1, res2.length);
assertEqual("10", res2[0]);
for (let x of res1) {
let res3 = db._query(q3, {key: x._key}).toArray();
assertEqual(1, res3.length);
assertEqual(x.value, res3[0]);
}
},
testVerticesAqlInsert: function () {
// Precondition
assertEqual(100, db[vertices].count());
let insert = `FOR i IN 0..99 INSERT {value: TO_STRING(i), needUpdate: true, needRemove: true} INTO ${vertices}`;
let update = `FOR x IN ${vertices} FILTER x.needUpdate UPDATE x WITH {needUpdate: false} INTO ${vertices}`;
let remove = `FOR x IN ${vertices} FILTER x.needRemove REMOVE x INTO ${vertices}`;
// Note: Order is important here, we first insert, than update those inserted docs, then remove them again
let resIns = db._query(insert);
assertEqual(100, resIns.getExtra().stats.writesExecuted);
assertEqual(0, resIns.getExtra().stats.writesIgnored);
assertEqual(200, db[vertices].count());
let resUp = db._query(update);
assertEqual(100, resUp.getExtra().stats.writesExecuted);
assertEqual(0, resUp.getExtra().stats.writesIgnored);
assertEqual(200, db[vertices].count());
let resRem = db._query(remove);
assertEqual(100, resRem.getExtra().stats.writesExecuted);
assertEqual(0, resRem.getExtra().stats.writesIgnored);
assertEqual(100, db[vertices].count());
},
testOrphans : function () {
let c = db._collection(orphans);
let p = c.properties();
assertEqual(2, c.type()); // Document
assertEqual(5, p.numberOfShards);
assertTrue(p.isSmart);
assertEqual(vertices, p.distributeShardsLike);
assertEqual(100, c.count());
assertEqual("value", p.smartGraphAttribute);
},
testOrphansAqlRead: function () {
let q1 = `FOR x IN ${orphans} SORT TO_NUMBER(x.value) RETURN x`;
let q2 = `FOR x IN ${orphans} FILTER x.value == "10" RETURN x.value`;
// This query can be optimized to a single shard. Make sure that is still correct
let q3 = `FOR x IN ${orphans} FILTER x._key == @key RETURN x.value`;
let res1 = db._query(q1).toArray();
assertEqual(100, res1.length);
for (let i = 0; i < 100; ++i) {
assertEqual(String(i), res1[i].value);
}
let res2 = db._query(q2).toArray();
assertEqual(1, res2.length);
assertEqual("10", res2[0]);
for (let x of res1) {
let res3 = db._query(q3, {key: x._key}).toArray();
assertEqual(1, res3.length);
assertEqual(x.value, res3[0]);
}
},
testOrphansAqlInsert: function () {
// Precondition
let c = db[orphans];
assertEqual(100, c.count());
let insert = `FOR i IN 0..99 INSERT {value: TO_STRING(i), needUpdate: true, needRemove: true} INTO ${orphans}`;
let update = `FOR x IN ${orphans} FILTER x.needUpdate UPDATE x WITH {needUpdate: false} INTO ${orphans}`;
let remove = `FOR x IN ${orphans} FILTER x.needRemove REMOVE x INTO ${orphans}`;
// Note: Order is important here, we first insert, than update those inserted docs, then remoe them again
let resIns = db._query(insert);
assertEqual(100, resIns.getExtra().stats.writesExecuted);
assertEqual(0, resIns.getExtra().stats.writesIgnored);
assertEqual(200, c.count());
let resUp = db._query(update);
assertEqual(100, resUp.getExtra().stats.writesExecuted);
assertEqual(0, resUp.getExtra().stats.writesIgnored);
assertEqual(200, c.count());
let resRem = db._query(remove);
assertEqual(100, resRem.getExtra().stats.writesExecuted);
assertEqual(0, resRem.getExtra().stats.writesIgnored);
assertEqual(100, c.count());
},
testEdges : function () {
let c = db._collection(edges);
let p = c.properties();
assertEqual(3, c.type()); // Edges
//assertEqual(5, p.numberOfShards);
assertTrue(p.isSmart);
assertEqual(vertices, p.distributeShardsLike);
assertEqual(300, c.count());
},
testEdgesAqlRead: function () {
let q1 = `FOR x IN ${edges} SORT TO_NUMBER(x.value) RETURN x`;
let q2 = `FOR x IN ${edges} FILTER x.value == "10" RETURN x.value`;
// This query can be optimized to a single shard. Make sure that is still correct
let q3 = `FOR x IN ${edges} FILTER x._key == @key RETURN x.value`;
let res1 = db._query(q1).toArray();
assertEqual(300, res1.length);
for (let i = 0; i < 100; ++i) {
// We have three edges per value
assertEqual(String(i), res1[3*i].value);
assertEqual(String(i), res1[3*i+1].value);
assertEqual(String(i), res1[3*i+2].value);
}
let res2 = db._query(q2).toArray();
assertEqual(3, res2.length);
assertEqual("10", res2[0]);
for (let x of res1) {
let res3 = db._query(q3, {key: x._key}).toArray();
assertEqual(1, res3.length);
assertEqual(x.value, res3[0]);
}
},
testEdgesAqlInsert: function () {
// Precondition
let c = db[edges];
assertEqual(300, c.count());
// We first need the vertices
let vC = db[vertices];
assertEqual(100, vC.count());
let vQ = `FOR x IN ${vertices} SORT TO_NUMBER(x.value) RETURN x._id`;
let verticesList = db._query(vQ).toArray();
let insertSameValue = `LET vs = @vertices FOR i IN 0..99 INSERT {_from: vs[i], _to: vs[i], value: TO_STRING(i), needUpdate: true, needRemove: true} INTO ${edges}`;
let insertOtherValue = `LET vs = @vertices FOR i IN 0..99 INSERT {_from: vs[i], _to: vs[(i + 1) % 100], value: TO_STRING(i), needUpdate: true, needRemove: true} INTO ${edges}`;
let update = `FOR x IN ${edges} FILTER x.needUpdate UPDATE x WITH {needUpdate: false} INTO ${edges}`;
let remove = `FOR x IN ${edges} FILTER x.needRemove REMOVE x INTO ${edges}`;
// Note: Order is important here, we first insert, than update those inserted docs, then remoe them again
let resInsSame = db._query(insertSameValue, {vertices: verticesList});
assertEqual(100, resInsSame.getExtra().stats.writesExecuted);
assertEqual(0, resInsSame.getExtra().stats.writesIgnored);
assertEqual(400, c.count());
let resInsOther = db._query(insertOtherValue, {vertices: verticesList});
assertEqual(100, resInsOther.getExtra().stats.writesExecuted);
assertEqual(0, resInsOther.getExtra().stats.writesIgnored);
assertEqual(500, c.count());
let resUp = db._query(update);
assertEqual(200, resUp.getExtra().stats.writesExecuted);
assertEqual(0, resUp.getExtra().stats.writesIgnored);
assertEqual(500, c.count());
let resRem = db._query(remove);
assertEqual(200, resRem.getExtra().stats.writesExecuted);
assertEqual(0, resRem.getExtra().stats.writesIgnored);
assertEqual(300, c.count());
},
testAqlGraphQuery: function() {
// Precondition
let c = db[edges];
assertEqual(300, c.count());
// We first need the vertices
let vC = db[vertices];
assertEqual(100, vC.count());
let vertexQuery = `FOR x IN ${vertices} FILTER x.value == "10" RETURN x._id`
let vertex = db._query(vertexQuery).toArray();
assertEqual(1, vertex.length);
let q = `FOR v IN 1..2 ANY "${vertex[0]}" GRAPH "${smartGraphName}" OPTIONS {uniqueVertices: 'path'} SORT TO_NUMBER(v.value) RETURN v`;
/* We expect the following result:
* 10 <- 9 <- 8
* 10 <- 9
* 10 -> 11
* 10 -> 11 -> 12
*/
//Validate that everything is wired to a smart graph correctly
let res = db._query(q).toArray();
assertEqual(4, res.length)
assertEqual("8", res[0].value);
assertEqual("9", res[1].value);
assertEqual("11", res[2].value);
assertEqual("12", res[3].value);
}
};
}
////////////////////////////////////////////////////////////////////////////////
/// @brief executes the test suite
////////////////////////////////////////////////////////////////////////////////
jsunity.run(dumpTestSuite);
if (isEnterprise) {
jsunity.run(dumpTestEnterpriseSuite);
}
return jsunity.done();

View File

@ -28,8 +28,11 @@
/// @author Copyright 2012, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////
var internal = require("internal");
var jsunity = require("jsunity");
const fs = require('fs');
const internal = require("internal");
const jsunity = require("jsunity");
const isEnterprise = internal.isEnterprise();
const db = internal.db;
////////////////////////////////////////////////////////////////////////////////
/// @brief test suite
@ -37,7 +40,6 @@ var jsunity = require("jsunity");
function dumpTestSuite () {
'use strict';
var db = internal.db;
return {
@ -302,11 +304,300 @@ function dumpTestSuite () {
};
}
////////////////////////////////////////////////////////////////////////////////
/// @brief test suite for the enterprise mode
////////////////////////////////////////////////////////////////////////////////
function dumpTestEnterpriseSuite () {
const smartGraphName = "UnitTestDumpSmartGraph";
const edges = "UnitTestDumpSmartEdges";
const vertices = "UnitTestDumpSmartVertices";
const orphans = "UnitTestDumpSmartOrphans";
const satellite = "UnitTestDumpSatelliteCollection";
const gm = require("@arangodb/smart-graph");
const instanceInfo = JSON.parse(require('internal').env.INSTANCEINFO);
return {
////////////////////////////////////////////////////////////////////////////////
/// @brief set up
////////////////////////////////////////////////////////////////////////////////
setUp : function () {
},
////////////////////////////////////////////////////////////////////////////////
/// @brief tear down
////////////////////////////////////////////////////////////////////////////////
tearDown : function () {
},
testSatelliteCollections : function () {
let c = db._collection(satellite);
let p = c.properties();
assertEqual(2, c.type()); // Document
assertEqual(1, p.numberOfShards);
assertEqual("satellite", p.replicationFactor);
assertEqual(100, c.count());
},
testHiddenCollectionsOmitted : function () {
const dumpDir = fs.join(instanceInfo.rootDir, 'dump');
const smartEdgeCollectionPath = fs.join(dumpDir, `${edges}.structure.json`);
const localEdgeCollectionPath = fs.join(dumpDir, `_local_${edges}.structure.json`);
const fromEdgeCollectionPath = fs.join(dumpDir, `_from_${edges}.structure.json`);
const toEdgeCollectionPath = fs.join(dumpDir, `_to_${edges}.structure.json`);
assertTrue(fs.exists(smartEdgeCollectionPath), 'Smart edge collection missing in dump!');
assertFalse(fs.exists(localEdgeCollectionPath), '_local edge collection should not have been dumped!');
assertFalse(fs.exists(fromEdgeCollectionPath), '_from edge collection should not have been dumped!');
assertFalse(fs.exists(toEdgeCollectionPath), '_to edge collection should not have been dumped!');
},
testShadowCollectionsOmitted : function () {
const dumpDir = fs.join(instanceInfo.rootDir, 'dump');
const collStructure = JSON.parse(
fs.read(fs.join(dumpDir, `${edges}.structure.json`))
);
assertTrue(collStructure.hasOwnProperty('parameters'), collStructure);
const parameters = collStructure['parameters'];
assertFalse(parameters.hasOwnProperty('shadowCollections'),
`Property 'shadowCollections' should be hidden in collection ${edges}!`);
},
testVertices : function () {
let c = db._collection(vertices);
let p = c.properties();
assertEqual(2, c.type()); // Document
assertEqual(5, p.numberOfShards);
assertTrue(p.isSmart, p);
assertFalse(Object.hasOwnProperty(p, "distributeShardsLike"));
assertEqual(100, c.count());
assertEqual("value", p.smartGraphAttribute);
},
testVerticesAqlRead: function () {
let q1 = `FOR x IN ${vertices} SORT TO_NUMBER(x.value) RETURN x`;
let q2 = `FOR x IN ${vertices} FILTER x.value == "10" RETURN x.value`;
// This query can be optimized to a single shard. Make sure that is still correct
let q3 = `FOR x IN ${vertices} FILTER x._key == @key RETURN x.value`;
let res1 = db._query(q1).toArray();
assertEqual(100, res1.length);
for (let i = 0; i < 100; ++i) {
assertEqual(String(i), res1[i].value);
}
let res2 = db._query(q2).toArray();
assertEqual(1, res2.length);
assertEqual("10", res2[0]);
for (let x of res1) {
let res3 = db._query(q3, {key: x._key}).toArray();
assertEqual(1, res3.length);
assertEqual(x.value, res3[0]);
}
},
testVerticesAqlInsert: function () {
// Precondition
assertEqual(100, db[vertices].count());
let insert = `FOR i IN 0..99 INSERT {value: TO_STRING(i), needUpdate: true, needRemove: true} INTO ${vertices}`;
let update = `FOR x IN ${vertices} FILTER x.needUpdate UPDATE x WITH {needUpdate: false} INTO ${vertices}`;
let remove = `FOR x IN ${vertices} FILTER x.needRemove REMOVE x INTO ${vertices}`;
// Note: Order is important here, we first insert, than update those inserted docs, then remoe them again
let resIns = db._query(insert);
assertEqual(100, resIns.getExtra().stats.writesExecuted);
assertEqual(0, resIns.getExtra().stats.writesIgnored);
assertEqual(200, db[vertices].count());
let resUp = db._query(update);
assertEqual(100, resUp.getExtra().stats.writesExecuted);
assertEqual(0, resUp.getExtra().stats.writesIgnored);
assertEqual(200, db[vertices].count());
let resRem = db._query(remove);
assertEqual(100, resRem.getExtra().stats.writesExecuted);
assertEqual(0, resRem.getExtra().stats.writesIgnored);
assertEqual(100, db[vertices].count());
},
testOrphans : function () {
let c = db._collection(orphans);
let p = c.properties();
assertEqual(2, c.type()); // Document
assertEqual(5, p.numberOfShards);
assertTrue(p.isSmart);
assertEqual(vertices, p.distributeShardsLike);
assertEqual(100, c.count());
assertEqual("value", p.smartGraphAttribute);
},
testOrphansAqlRead: function () {
let q1 = `FOR x IN ${orphans} SORT TO_NUMBER(x.value) RETURN x`;
let q2 = `FOR x IN ${orphans} FILTER x.value == "10" RETURN x.value`;
// This query can be optimized to a single shard. Make sure that is still correct
let q3 = `FOR x IN ${orphans} FILTER x._key == @key RETURN x.value`;
let res1 = db._query(q1).toArray();
assertEqual(100, res1.length);
for (let i = 0; i < 100; ++i) {
assertEqual(String(i), res1[i].value);
}
let res2 = db._query(q2).toArray();
assertEqual(1, res2.length);
assertEqual("10", res2[0]);
for (let x of res1) {
let res3 = db._query(q3, {key: x._key}).toArray();
assertEqual(1, res3.length);
assertEqual(x.value, res3[0]);
}
},
testOrphansAqlInsert: function () {
// Precondition
let c = db[orphans];
assertEqual(100, c.count());
let insert = `FOR i IN 0..99 INSERT {value: TO_STRING(i), needUpdate: true, needRemove: true} INTO ${orphans}`;
let update = `FOR x IN ${orphans} FILTER x.needUpdate UPDATE x WITH {needUpdate: false} INTO ${orphans}`;
let remove = `FOR x IN ${orphans} FILTER x.needRemove REMOVE x INTO ${orphans}`;
// Note: Order is important here, we first insert, than update those inserted docs, then remoe them again
let resIns = db._query(insert);
assertEqual(100, resIns.getExtra().stats.writesExecuted);
assertEqual(0, resIns.getExtra().stats.writesIgnored);
assertEqual(200, c.count());
let resUp = db._query(update);
assertEqual(100, resUp.getExtra().stats.writesExecuted);
assertEqual(0, resUp.getExtra().stats.writesIgnored);
assertEqual(200, c.count());
let resRem = db._query(remove);
assertEqual(100, resRem.getExtra().stats.writesExecuted);
assertEqual(0, resRem.getExtra().stats.writesIgnored);
assertEqual(100, c.count());
},
testEdges : function () {
let c = db._collection(edges);
let p = c.properties();
assertEqual(3, c.type()); // Edges
//assertEqual(5, p.numberOfShards);
assertTrue(p.isSmart);
assertEqual(vertices, p.distributeShardsLike);
assertEqual(300, c.count());
},
testEdgesAqlRead: function () {
let q1 = `FOR x IN ${edges} SORT TO_NUMBER(x.value) RETURN x`;
let q2 = `FOR x IN ${edges} FILTER x.value == "10" RETURN x.value`;
// This query can be optimized to a single shard. Make sure that is still correct
let q3 = `FOR x IN ${edges} FILTER x._key == @key RETURN x.value`;
let res1 = db._query(q1).toArray();
assertEqual(300, res1.length);
for (let i = 0; i < 100; ++i) {
// We have three edges per value
assertEqual(String(i), res1[3*i].value);
assertEqual(String(i), res1[3*i+1].value);
assertEqual(String(i), res1[3*i+2].value);
}
let res2 = db._query(q2).toArray();
assertEqual(3, res2.length);
assertEqual("10", res2[0]);
for (let x of res1) {
let res3 = db._query(q3, {key: x._key}).toArray();
assertEqual(1, res3.length);
assertEqual(x.value, res3[0]);
}
},
testEdgesAqlInsert: function () {
// Precondition
let c = db[edges];
assertEqual(300, c.count());
// We first need the vertices
let vC = db[vertices];
assertEqual(100, vC.count());
let vQ = `FOR x IN ${vertices} SORT TO_NUMBER(x.value) RETURN x._id`;
let verticesList = db._query(vQ).toArray();
let insertSameValue = `LET vs = @vertices FOR i IN 0..99 INSERT {_from: vs[i], _to: vs[i], value: TO_STRING(i), needUpdate: true, needRemove: true} INTO ${edges}`;
let insertOtherValue = `LET vs = @vertices FOR i IN 0..99 INSERT {_from: vs[i], _to: vs[(i + 1) % 100], value: TO_STRING(i), needUpdate: true, needRemove: true} INTO ${edges}`;
let update = `FOR x IN ${edges} FILTER x.needUpdate UPDATE x WITH {needUpdate: false} INTO ${edges}`;
let remove = `FOR x IN ${edges} FILTER x.needRemove REMOVE x INTO ${edges}`;
// Note: Order is important here, we first insert, than update those inserted docs, then remove them again
let resInsSame = db._query(insertSameValue, {vertices: verticesList});
assertEqual(100, resInsSame.getExtra().stats.writesExecuted);
assertEqual(0, resInsSame.getExtra().stats.writesIgnored);
assertEqual(400, c.count());
let resInsOther = db._query(insertOtherValue, {vertices: verticesList});
assertEqual(100, resInsOther.getExtra().stats.writesExecuted);
assertEqual(0, resInsOther.getExtra().stats.writesIgnored);
assertEqual(500, c.count());
let resUp = db._query(update);
assertEqual(200, resUp.getExtra().stats.writesExecuted);
assertEqual(0, resUp.getExtra().stats.writesIgnored);
assertEqual(500, c.count());
let resRem = db._query(remove);
assertEqual(200, resRem.getExtra().stats.writesExecuted);
assertEqual(0, resRem.getExtra().stats.writesIgnored);
assertEqual(300, c.count());
},
testAqlGraphQuery: function() {
// Precondition
let c = db[edges];
assertEqual(300, c.count());
// We first need the vertices
let vC = db[vertices];
assertEqual(100, vC.count());
let vertexQuery = `FOR x IN ${vertices} FILTER x.value == "10" RETURN x._id`
let vertex = db._query(vertexQuery).toArray();
assertEqual(1, vertex.length);
let q = `FOR v IN 1..2 ANY "${vertex[0]}" GRAPH "${smartGraphName}" OPTIONS {uniqueVertices: 'path'} SORT TO_NUMBER(v.value) RETURN v`;
/* We expect the following result:
* 10 <- 9 <- 8
* 10 <- 9
* 10 -> 11
* 10 -> 11 -> 12
*/
//Validate that everything is wired to a smart graph correctly
let res = db._query(q).toArray();
assertEqual(4, res.length)
assertEqual("8", res[0].value);
assertEqual("9", res[1].value);
assertEqual("11", res[2].value);
assertEqual("12", res[3].value);
}
};
}
////////////////////////////////////////////////////////////////////////////////
/// @brief executes the test suite
////////////////////////////////////////////////////////////////////////////////
jsunity.run(dumpTestSuite);
return jsunity.done();
if (isEnterprise) {
jsunity.run(dumpTestEnterpriseSuite);
}
return jsunity.done();

View File

@ -27,9 +27,75 @@
/// @author Copyright 2012, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////
'use strict';
const db = require("@arangodb").db;
const isEnterprise = require("internal").isEnterprise();
/**
* @brief Only if enterprise mode:
* Creates a smart graph sharded by `value`
* That has 100 vertices (value 0 -> 99)
* That has 100 orphans (value 0 -> 99)
* That has 300 edges, for each value i:
* Connect i -> i
* Connect i - 1 -> i
* Connect i -> i + 1
*/
const setupSmartGraph = function () {
if (!isEnterprise) {
return;
}
const smartGraphName = "UnitTestDumpSmartGraph";
const edges = "UnitTestDumpSmartEdges";
const vertices = "UnitTestDumpSmartVertices";
const orphans = "UnitTestDumpSmartOrphans";
const gm = require("@arangodb/smart-graph");
if (gm._exists(smartGraphName)) {
gm._drop(smartGraphName, true);
}
db._drop(edges);
db._drop(vertices);
gm._create(smartGraphName, [gm._relation(edges, vertices, vertices)],
[orphans], {numberOfShards: 5, smartGraphAttribute: "value"});
let vDocs = [];
for (let i = 0; i < 100; ++i) {
vDocs.push({value: String(i)});
}
let saved = db[vertices].save(vDocs).map(v => v._id);
let eDocs = [];
for (let i = 0; i < 100; ++i) {
eDocs.push({_from: saved[(i+1) % 100], _to: saved[i], value: String(i)});
eDocs.push({_from: saved[i], _to: saved[i], value: String(i)});
eDocs.push({_from: saved[i], _to: saved[(i+1) % 100], value: String(i)});
}
db[edges].save(eDocs);
db[orphans].save(vDocs);
};
/**
* @brief Only if enterprise mode:
* Creates a satellite collection with 100 documents
*/
function setupSatelliteCollections() {
if (!isEnterprise) {
return;
}
const satelliteCollectionName = "UnitTestDumpSatelliteCollection";
db._drop(satelliteCollectionName);
db._create(satelliteCollectionName, {"replicationFactor": "satellite"});
let vDocs = [];
for (let i = 0; i < 100; ++i) {
vDocs.push({value: String(i)});
}
db[satelliteCollectionName].save(vDocs);
}
(function () {
'use strict';
var db = require("@arangodb").db;
var i, c;
try {
@ -153,6 +219,8 @@
c.save({ _key: "text" + i, value: t });
});
setupSmartGraph();
setupSatelliteCollections();
})();
return {