Skip to content

Commit 601db82

Browse files
authored
MONGOSH-308 - MapReduce and Validate (#321)
1 parent 4a0ae0d commit 601db82

File tree

5 files changed

+242
-1
lines changed

5 files changed

+242
-1
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/i18n/src/locales/en_US.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,16 @@ const translations = {
556556
description: 'Returns an interface to access the query plan cache for a collection. The interface provides methods to view and clear the query plan cache.',
557557
example: 'db.coll.getPlanCache()'
558558
},
559+
validate: {
560+
link: 'https://docs.mongodb.com/manual/reference/method/db.collection.validate',
561+
description: 'Calls the validate command. Default full value is false',
562+
example: 'db.validate(<full>)'
563+
},
564+
mapReduce: {
565+
link: 'https://docs.mongodb.com/manual/reference/method/db.collection.mapReduce',
566+
description: 'Calls the mapReduce command',
567+
example: 'db.mapReduce(mapFn, reduceFn, options)'
568+
}
559569
}
560570
}
561571
},

packages/shell-api/src/collection.spec.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,5 +958,94 @@ describe('Collection', () => {
958958
expect(pc._asPrintable()).to.equal('PlanCache for collection coll1.');
959959
});
960960
});
961+
describe('validate', () => {
962+
it('calls serviceProvider.runCommand on the collection default', async() => {
963+
serviceProvider.runCommandWithCheck.resolves({ ok: 1 });
964+
await collection.validate();
965+
expect(serviceProvider.runCommandWithCheck).to.have.been.calledWith(
966+
database._name,
967+
{
968+
validate: collection._name,
969+
full: false
970+
}
971+
);
972+
});
973+
it('calls serviceProvider.runCommand on the collection with options', async() => {
974+
await collection.validate(true);
975+
expect(serviceProvider.runCommandWithCheck).to.have.been.calledWith(
976+
database._name,
977+
{
978+
validate: collection._name,
979+
full: true
980+
}
981+
);
982+
});
983+
984+
it('returns whatever serviceProvider.runCommand returns', async() => {
985+
const expectedResult = { ok: 1 };
986+
serviceProvider.runCommandWithCheck.resolves(expectedResult);
987+
const result = await collection.validate();
988+
expect(result).to.deep.equal(expectedResult);
989+
});
990+
991+
it('throws if serviceProvider.runCommand rejects', async() => {
992+
const expectedError = new Error();
993+
serviceProvider.runCommandWithCheck.rejects(expectedError);
994+
const catchedError = await collection.validate()
995+
.catch(e => e);
996+
expect(catchedError).to.equal(expectedError);
997+
});
998+
});
999+
describe('mapReduce', () => {
1000+
let mapFn;
1001+
let reduceFn;
1002+
beforeEach(() => {
1003+
mapFn = function(): void {};
1004+
reduceFn = function(keyCustId, valuesPrices): any {
1005+
return valuesPrices.reduce((t, s) => (t + s));
1006+
};
1007+
});
1008+
it('calls serviceProvider.mapReduce on the collection with js args', async() => {
1009+
serviceProvider.runCommandWithCheck.resolves({ ok: 1 });
1010+
await collection.mapReduce(mapFn, reduceFn, { out: 'map_reduce_example' });
1011+
expect(serviceProvider.runCommandWithCheck).to.have.been.calledWith(
1012+
database._name,
1013+
{
1014+
mapReduce: collection._name,
1015+
map: mapFn,
1016+
reduce: reduceFn,
1017+
out: 'map_reduce_example'
1018+
}
1019+
);
1020+
});
1021+
it('calls serviceProvider.runCommand on the collection with string args', async() => {
1022+
serviceProvider.runCommandWithCheck.resolves({ ok: 1 });
1023+
await collection.mapReduce(mapFn.toString(), reduceFn.toString(), { out: 'map_reduce_example' });
1024+
expect(serviceProvider.runCommandWithCheck).to.have.been.calledWith(
1025+
database._name,
1026+
{
1027+
mapReduce: collection._name,
1028+
map: mapFn.toString(),
1029+
reduce: reduceFn.toString(),
1030+
out: 'map_reduce_example'
1031+
}
1032+
);
1033+
});
1034+
1035+
it('returns whatever serviceProvider.mapReduce returns', async() => {
1036+
const expectedResult = { ok: 1 };
1037+
serviceProvider.runCommandWithCheck.resolves(expectedResult);
1038+
const result = await collection.mapReduce(mapFn, reduceFn, { out: { inline: 1 } });
1039+
expect(result).to.deep.equal(expectedResult);
1040+
});
1041+
1042+
it('throws if serviceProvider.mapReduce rejects', async() => {
1043+
const expectedError = new Error();
1044+
serviceProvider.runCommandWithCheck.rejects(expectedError);
1045+
const catchedError = await collection.mapReduce(mapFn, reduceFn, { out: { inline: 1 } })
1046+
.catch(e => e);
1047+
expect(catchedError).to.equal(expectedError);
1048+
});
1049+
});
9611050
});
9621051
});

packages/shell-api/src/collection.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,4 +1364,41 @@ export default class Collection extends ShellApiClass {
13641364
this._emitCollectionApiCall('getPlanCache');
13651365
return new PlanCache(this);
13661366
}
1367+
1368+
@returnsPromise
1369+
async mapReduce(map: any, reduce: any, optionsOrOutString: Document | string): Promise<any> {
1370+
assertArgsDefined(map, reduce, optionsOrOutString);
1371+
this._emitCollectionApiCall('mapReduce', { map, reduce, out: optionsOrOutString });
1372+
1373+
let cmd = {
1374+
mapReduce: this._name,
1375+
map: map,
1376+
reduce: reduce
1377+
} as any;
1378+
1379+
if (typeof optionsOrOutString === 'string') {
1380+
cmd.out = optionsOrOutString;
1381+
} else if (optionsOrOutString.out === undefined) {
1382+
throw new MongoshInvalidInputError('Missing \'out\' option');
1383+
} else {
1384+
cmd = { ...cmd, ...optionsOrOutString };
1385+
}
1386+
1387+
return await this._mongo._serviceProvider.runCommandWithCheck(
1388+
this._database._name,
1389+
cmd
1390+
);
1391+
}
1392+
1393+
@returnsPromise
1394+
async validate(full = false): Promise<Document> {
1395+
this._emitCollectionApiCall('validate', { full });
1396+
return await this._mongo._serviceProvider.runCommandWithCheck(
1397+
this._database._name,
1398+
{
1399+
validate: this._name,
1400+
full: full
1401+
}
1402+
);
1403+
}
13671404
}

packages/shell-api/src/integration.spec.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @typescript-eslint/camelcase */
12
import { expect } from 'chai';
23
import { CliServiceProvider } from '../../service-provider-server'; // avoid cyclic dep just for test
34
import ShellInternalState from './shell-internal-state';
@@ -66,6 +67,22 @@ describe('Shell API (integration)', function() {
6667
await collection.find( { quantity: { $gte: 5 }, type: 'apparel' } ).toArray();
6768
};
6869

70+
const loadMRExample = async(collection): Promise<any> => {
71+
const res = await collection.insertMany([
72+
{ _id: 1, cust_id: 'Ant O. Knee', ord_date: new Date('2020-03-01'), price: 25, items: [ { sku: 'oranges', qty: 5, price: 2.5 }, { sku: 'apples', qty: 5, price: 2.5 } ], status: 'A' },
73+
{ _id: 2, cust_id: 'Ant O. Knee', ord_date: new Date('2020-03-08'), price: 70, items: [ { sku: 'oranges', qty: 8, price: 2.5 }, { sku: 'chocolates', qty: 5, price: 10 } ], status: 'A' },
74+
{ _id: 3, cust_id: 'Busby Bee', ord_date: new Date('2020-03-08'), price: 50, items: [ { sku: 'oranges', qty: 10, price: 2.5 }, { sku: 'pears', qty: 10, price: 2.5 } ], status: 'A' },
75+
{ _id: 4, cust_id: 'Busby Bee', ord_date: new Date('2020-03-18'), price: 25, items: [ { sku: 'oranges', qty: 10, price: 2.5 } ], status: 'A' },
76+
{ _id: 5, cust_id: 'Busby Bee', ord_date: new Date('2020-03-19'), price: 50, items: [ { sku: 'chocolates', qty: 5, price: 10 } ], status: 'A' },
77+
{ _id: 6, cust_id: 'Cam Elot', ord_date: new Date('2020-03-19'), price: 35, items: [ { sku: 'carrots', qty: 10, price: 1.0 }, { sku: 'apples', qty: 10, price: 2.5 } ], status: 'A' },
78+
{ _id: 7, cust_id: 'Cam Elot', ord_date: new Date('2020-03-20'), price: 25, items: [ { sku: 'oranges', qty: 10, price: 2.5 } ], status: 'A' },
79+
{ _id: 8, cust_id: 'Don Quis', ord_date: new Date('2020-03-20'), price: 75, items: [ { sku: 'chocolates', qty: 5, price: 10 }, { sku: 'apples', qty: 10, price: 2.5 } ], status: 'A' },
80+
{ _id: 9, cust_id: 'Don Quis', ord_date: new Date('2020-03-20'), price: 55, items: [ { sku: 'carrots', qty: 5, price: 1.0 }, { sku: 'apples', qty: 10, price: 2.5 }, { sku: 'oranges', qty: 10, price: 2.5 } ], status: 'A' },
81+
{ _id: 10, cust_id: 'Don Quis', ord_date: new Date('2020-03-23'), price: 25, items: [ { sku: 'oranges', qty: 10, price: 2.5 } ], status: 'A' }
82+
]);
83+
expect(res.acknowledged).to.equal(1);
84+
};
85+
6986
before(async() => {
7087
serviceProvider = await CliServiceProvider.connect(connectionString);
7188
});
@@ -1348,4 +1365,92 @@ describe('Shell API (integration)', function() {
13481365
});
13491366
});
13501367
});
1368+
describe('mapReduce', () => {
1369+
it('accepts function args and collection name as string', async() => {
1370+
await loadMRExample(collection);
1371+
const mapFn = `function() {
1372+
emit(this.cust_id, this.price);
1373+
};`;
1374+
const reduceFn = function(keyCustId, valuesPrices): any {
1375+
return valuesPrices.reduce((s, t) => s + t);
1376+
};
1377+
const result = await collection.mapReduce(mapFn, reduceFn, 'map_reduce_example');
1378+
expect(result.ok).to.equal(1);
1379+
const outRes = await database.map_reduce_example.find().sort({ _id: 1 }).toArray();
1380+
expect(outRes).to.deep.equal([
1381+
{ '_id': 'Ant O. Knee', 'value': 95 },
1382+
{ '_id': 'Busby Bee', 'value': 125 },
1383+
{ '_id': 'Cam Elot', 'value': 60 },
1384+
{ '_id': 'Don Quis', 'value': 155 }
1385+
]);
1386+
});
1387+
it('accepts string args and collection name as string', async() => {
1388+
await loadMRExample(collection);
1389+
const mapFn = `function() {
1390+
emit(this.cust_id, this.price);
1391+
};`;
1392+
const reduceFn = function(keyCustId, valuesPrices): any {
1393+
return valuesPrices.reduce((s, t) => s + t);
1394+
};
1395+
const result = await collection.mapReduce(mapFn, reduceFn.toString(), 'map_reduce_example');
1396+
expect(result.ok).to.equal(1);
1397+
expect(result.result).to.equal('map_reduce_example');
1398+
const outRes = await database.map_reduce_example.find().sort({ _id: 1 }).toArray();
1399+
expect(outRes).to.deep.equal([
1400+
{ '_id': 'Ant O. Knee', 'value': 95 },
1401+
{ '_id': 'Busby Bee', 'value': 125 },
1402+
{ '_id': 'Cam Elot', 'value': 60 },
1403+
{ '_id': 'Don Quis', 'value': 155 }
1404+
]);
1405+
});
1406+
it('accepts inline as option', async() => {
1407+
await loadMRExample(collection);
1408+
const mapFn = `function() {
1409+
emit(this.cust_id, this.price);
1410+
};`;
1411+
const reduceFn = function(keyCustId, valuesPrices): any {
1412+
return valuesPrices.reduce((s, t) => s + t);
1413+
};
1414+
const result = await collection.mapReduce(mapFn, reduceFn.toString(), {
1415+
out: { inline: 1 }
1416+
});
1417+
expect(result.ok).to.equal(1);
1418+
expect(result.results.map(k => k._id).sort()).to.deep.equal([
1419+
'Ant O. Knee',
1420+
'Busby Bee',
1421+
'Cam Elot',
1422+
'Don Quis'
1423+
]);
1424+
expect(result.results.map(k => k.value).sort()).to.deep.equal([
1425+
125,
1426+
155,
1427+
60,
1428+
95
1429+
]);
1430+
});
1431+
it('accepts finalize as option', async() => {
1432+
await loadMRExample(collection);
1433+
const mapFn = `function() {
1434+
emit(this.cust_id, this.price);
1435+
};`;
1436+
const reduceFn = function(keyCustId, valuesPrices): any {
1437+
return valuesPrices.reduce((s, t) => s + t);
1438+
};
1439+
const finalizeFn = function(): any {
1440+
return 1;
1441+
};
1442+
const result = await collection.mapReduce(mapFn, reduceFn.toString(), {
1443+
out: { inline: 1 },
1444+
finalize: finalizeFn
1445+
});
1446+
expect(result.ok).to.equal(1);
1447+
expect(result.results.map(k => k._id).sort()).to.deep.equal([
1448+
'Ant O. Knee',
1449+
'Busby Bee',
1450+
'Cam Elot',
1451+
'Don Quis'
1452+
]);
1453+
expect(result.results.map(k => k.value)).to.deep.equal([1, 1, 1, 1]);
1454+
});
1455+
});
13511456
});

0 commit comments

Comments
 (0)