Skip to content
This repository
Browse code

Merge pull request #64 from doctrine/group-options

Revise query builder API for map/reduce and group
  • Loading branch information...
commit 17e9c5a25c11a278af31ec6e2df1a401de367a91 2 parents b47c335 + 031e192
Jeremy Mikola jmikola authored
17 lib/Doctrine/MongoDB/Collection.php
@@ -539,9 +539,24 @@ public function group($keys, array $initial, $reduce, array $options = array())
539 539
540 540 protected function doGroup($keys, array $initial, $reduce, array $options)
541 541 {
  542 + if (is_string($reduce)) {
  543 + $reduce = new \MongoCode($reduce);
  544 + }
  545 +
  546 + if (isset($options['finalize']) && is_string($options['finalize'])) {
  547 + $options['finalize'] = new \MongoCode($options['finalize']);
  548 + }
  549 +
542 550 $collection = $this;
543 551 $result = $this->retry(function() use ($collection, $keys, $initial, $reduce, $options) {
544   - return $collection->getMongoCollection()->group($keys, $initial, $reduce, $options);
  552 + /* Version 1.2.11+ of the driver yields an E_DEPRECATED notice if an
  553 + * empty array is passed to MongoCollection::group(), as it assumes
  554 + * an it is the "condition" option's value being passed instead of
  555 + * a well-formed options array (the actual deprecated behavior).
  556 + */
  557 + return empty($options)
  558 + ? $collection->getMongoCollection()->group($keys, $initial, $reduce)
  559 + : $collection->getMongoCollection()->group($keys, $initial, $reduce, $options);
545 560 });
546 561 return new ArrayIterator($result);
547 562 }
68 lib/Doctrine/MongoDB/Query/Builder.php
@@ -65,7 +65,12 @@ class Builder
65 65 'sort' => array(),
66 66 'limit' => null,
67 67 'skip' => null,
68   - 'group' => array(),
  68 + 'group' => array(
  69 + 'keys' => null,
  70 + 'initial' => null,
  71 + 'reduce' => null,
  72 + 'options' => array(),
  73 + ),
69 74 'hints' => array(),
70 75 'immortal' => false,
71 76 'snapshot' => false,
@@ -74,7 +79,7 @@ class Builder
74 79 'mapReduce' => array(
75 80 'map' => null,
76 81 'reduce' => null,
77   - 'options' => array()
  82 + 'options' => array(),
78 83 ),
79 84 'near' => array(),
80 85 'new' => false,
@@ -277,17 +282,21 @@ public function remove()
277 282 /**
278 283 * Perform an operation similar to SQL's GROUP BY command
279 284 *
280   - * @param $keys
  285 + * @param mixed $keys
281 286 * @param array $initial
  287 + * @param string|MongoCode $reduce
  288 + * @param array $options
282 289 * @return Builder
283 290 */
284   - public function group($keys, array $initial)
  291 + public function group($keys, array $initial, $reduce = null, array $options = array())
285 292 {
  293 + $this->query['type'] = Query::TYPE_GROUP;
286 294 $this->query['group'] = array(
287 295 'keys' => $keys,
288   - 'initial' => $initial
  296 + 'initial' => $initial,
  297 + 'reduce' => $reduce,
  298 + 'options' => $options,
289 299 );
290   - $this->query['type'] = Query::TYPE_GROUP;
291 300 return $this;
292 301 }
293 302
@@ -692,8 +701,8 @@ public function skip($skip)
692 701 /**
693 702 * Specify a map reduce operation for this query.
694 703 *
695   - * @param string $map
696   - * @param string $reduce
  704 + * @param string|MongoCode $map
  705 + * @param string|MongoCode $reduce
697 706 * @param array $out
698 707 * @param array $options
699 708 * @return Builder
@@ -713,46 +722,67 @@ public function mapReduce($map, $reduce, array $out = array('inline' => true), a
713 722 /**
714 723 * Specify a map operation for this query.
715 724 *
716   - * @param string $map
  725 + * @param string|MongoCode $map
717 726 * @return Builder
718 727 */
719 728 public function map($map)
720 729 {
721   - $this->query['mapReduce']['map'] = $map;
722 730 $this->query['type'] = Query::TYPE_MAP_REDUCE;
  731 + $this->query['mapReduce']['map'] = $map;
723 732 return $this;
724 733 }
725 734
726 735 /**
727 736 * Specify a reduce operation for this query.
728 737 *
729   - * @param string $reduce
  738 + * @param string|MongoCode $reduce
730 739 * @return Builder
  740 + * @throws BadMethodCallException if the query type is unsupported
731 741 */
732 742 public function reduce($reduce)
733 743 {
734   - $this->query['mapReduce']['reduce'] = $reduce;
735   - if (isset($this->query['mapReduce']['map']) && isset($this->query['mapReduce']['reduce'])) {
736   - $this->query['type'] = Query::TYPE_MAP_REDUCE;
  744 + switch ($this->query['type']) {
  745 + case Query::TYPE_MAP_REDUCE:
  746 + $this->query['mapReduce']['reduce'] = $reduce;
  747 + break;
  748 +
  749 + case Query::TYPE_GROUP:
  750 + $this->query['group']['reduce'] = $reduce;
  751 + break;
  752 +
  753 + default:
  754 + throw new \BadMethodCallException('mapReduce(), map() or group() must be called before reduce()');
737 755 }
  756 +
738 757 return $this;
739 758 }
740 759
741 760 /**
742 761 * Specify a finalize operation for this query.
743 762 *
744   - * @param string $finalize
  763 + * @param string|MongoCode $finalize
745 764 * @return Builder
746 765 */
747 766 public function finalize($finalize)
748 767 {
749   - $this->query['mapReduce']['options']['finalize'] = $finalize;
750   - $this->query['type'] = Query::TYPE_MAP_REDUCE;
  768 + switch ($this->query['type']) {
  769 + case Query::TYPE_MAP_REDUCE:
  770 + $this->query['mapReduce']['options']['finalize'] = $finalize;
  771 + break;
  772 +
  773 + case Query::TYPE_GROUP:
  774 + $this->query['group']['options']['finalize'] = $finalize;
  775 + break;
  776 +
  777 + default:
  778 + throw new \BadMethodCallException('mapReduce(), map() or group() must be called before reduce()');
  779 + }
  780 +
751 781 return $this;
752 782 }
753 783
754 784 /**
755   - * Specify output type for mar/reduce operation.
  785 + * Specify output type for map/reduce operation.
756 786 *
757 787 * @param array $out
758 788 * @return Builder
@@ -760,8 +790,6 @@ public function finalize($finalize)
760 790 public function out(array $out)
761 791 {
762 792 $this->query['mapReduce']['out'] = $out;
763   - $this->query['type'] = Query::TYPE_MAP_REDUCE;
764   -
765 793 return $this;
766 794 }
767 795
11 lib/Doctrine/MongoDB/Query/Query.php
@@ -144,9 +144,6 @@ public function execute()
144 144 {
145 145 switch ($this->query['type']) {
146 146 case self::TYPE_FIND:
147   - if (isset($this->query['mapReduce']['reduce'])) {
148   - $this->query['query'][$this->cmd . 'where'] = $this->query['mapReduce']['reduce'];
149   - }
150 147 $cursor = $this->collection->find($this->query['query'], $this->query['select']);
151 148 return $this->prepareCursor($cursor);
152 149
@@ -190,7 +187,13 @@ public function execute()
190 187 return $this->collection->remove($this->query['query'], $this->options);
191 188
192 189 case self::TYPE_GROUP:
193   - return $this->collection->group($this->query['group']['keys'], $this->query['group']['initial'], $this->query['mapReduce']['reduce'], $this->query['query']);
  190 + if (!empty($this->query['query'])) {
  191 + $this->query['group']['options']['condition'] = $this->query['query'];
  192 + }
  193 +
  194 + $options = array_merge($this->options, $this->query['group']['options']);
  195 +
  196 + return $this->collection->group($this->query['group']['keys'], $this->query['group']['initial'], $this->query['group']['reduce'], $options);
194 197
195 198 case self::TYPE_MAP_REDUCE:
196 199 if (!isset($this->query['mapReduce']['out'])) {
26 tests/Doctrine/MongoDB/Tests/CollectionTest.php
@@ -312,18 +312,38 @@ public function testGetName()
312 312 $this->assertEquals(true, $result);
313 313 }
314 314
315   - public function testGroup()
  315 + public function testGroupWithNonEmptyOptionsArray()
  316 + {
  317 + $expectedOptions = array(
  318 + 'condition' => array(),
  319 + 'finalize' => new \MongoCode(''),
  320 + );
  321 +
  322 + $mockConnection = $this->getMockConnection();
  323 + $mongoCollection = $this->getMockMongoCollection();
  324 + $mongoCollection->expects($this->once())
  325 + ->method('group')
  326 + ->with(array(), array(), $this->isInstanceOf('MongoCode'), $this->equalTo($expectedOptions))
  327 + ->will($this->returnValue(array()));
  328 +
  329 + $mockDatabase = $this->getMockDatabase();
  330 + $coll = $this->getTestCollection($mockConnection, $mongoCollection, $mockDatabase);
  331 + $result = $coll->group(array(), array(), '', array('condition' => array(), 'finalize' => ''));
  332 + $this->assertEquals(new \Doctrine\MongoDB\ArrayIterator(array()), $result);
  333 + }
  334 +
  335 + public function testGroupWithEmptyOptionsArray()
316 336 {
317 337 $mockConnection = $this->getMockConnection();
318 338 $mongoCollection = $this->getMockMongoCollection();
319 339 $mongoCollection->expects($this->once())
320 340 ->method('group')
321   - ->with(array(), array(), '', array())
  341 + ->with(array(), array(), $this->isInstanceOf('MongoCode'))
322 342 ->will($this->returnValue(array()));
323 343
324 344 $mockDatabase = $this->getMockDatabase();
325 345 $coll = $this->getTestCollection($mockConnection, $mongoCollection, $mockDatabase);
326   - $result = $coll->group(array(), array(), '', array());
  346 + $result = $coll->group(array(), array(), '');
327 347 $this->assertEquals(new \Doctrine\MongoDB\ArrayIterator(array()), $result);
328 348 }
329 349
136 tests/Doctrine/MongoDB/Tests/Query/BuilderTest.php
@@ -38,7 +38,7 @@ public function testFindAndRemoveQuery()
38 38 $this->assertNull($qb->getQuery()->execute());
39 39 }
40 40
41   - public function testMapReduceQuery()
  41 + public function testMapReduceQueryWithSingleMethod()
42 42 {
43 43 $map = 'function() {
44 44 for(i = 0; i <= this.options.length; i++) {
@@ -60,16 +60,60 @@ public function testMapReduceQuery()
60 60
61 61 $finalize = 'function (key, value) { return value; }';
62 62
  63 + $out = array('inline' => true);
  64 +
63 65 $qb = $this->getTestQueryBuilder()
64   - ->map($map)->reduce($reduce)->finalize($finalize)
65   - ->field('username')->equals('jwage');
  66 + ->mapReduce($map, $reduce, $out, array('finalize' => $finalize));
  67 +
  68 + $expectedMapReduce = array(
  69 + 'map' => $map,
  70 + 'reduce' => $reduce,
  71 + 'out' => array('inline' => true),
  72 + 'options' => array('finalize' => $finalize),
  73 + );
66 74
67 75 $this->assertEquals(Query::TYPE_MAP_REDUCE, $qb->getType());
68   - $expected = array(
69   - 'username' => 'jwage'
  76 + $this->assertEquals($expectedMapReduce, $qb->debug('mapReduce'));
  77 + }
  78 +
  79 + public function testMapReduceQueryWithMultipleMethodsAndQueryArray()
  80 + {
  81 + $map = 'function() {
  82 + for(i = 0; i <= this.options.length; i++) {
  83 + emit(this.name, { count: 1 });
  84 + }
  85 + }';
  86 +
  87 + $reduce = 'function(product, values) {
  88 + var total = 0
  89 + values.forEach(function(value){
  90 + total+= value.count;
  91 + });
  92 + return {
  93 + product: product,
  94 + options: total,
  95 + test: values
  96 + };
  97 + }';
  98 +
  99 + $finalize = 'function (key, value) { return value; }';
  100 +
  101 + $qb = $this->getTestQueryBuilder()
  102 + ->map($map)
  103 + ->reduce($reduce)
  104 + ->finalize($finalize)
  105 + ->field('username')->equals('jwage');
  106 +
  107 + $expectedQueryArray = array('username' => 'jwage');
  108 + $expectedMapReduce = array(
  109 + 'map' => $map,
  110 + 'reduce' => $reduce,
  111 + 'options' => array('finalize' => $finalize),
70 112 );
71   - $this->assertEquals($expected, $qb->getQueryArray());
72   - $this->assertEquals(array('map' => $map, 'options' => array('finalize' => $finalize), 'reduce' => $reduce), $qb->debug('mapReduce'));
  113 +
  114 + $this->assertEquals(Query::TYPE_MAP_REDUCE, $qb->getType());
  115 + $this->assertEquals($expectedQueryArray, $qb->getQueryArray());
  116 + $this->assertEquals($expectedMapReduce, $qb->debug('mapReduce'));
73 117 }
74 118
75 119 public function testFindAndUpdateQuery()
@@ -89,12 +133,49 @@ public function testFindAndUpdateQuery()
89 133 $this->assertNull($query->execute());
90 134 }
91 135
92   - public function testGroupQuery()
  136 + public function testGroupQueryWithSingleMethod()
93 137 {
  138 + $keys = array();
  139 + $initial = array('count' => 0, 'sum' => 0);
  140 + $reduce = 'function(obj, prev) { prev.count++; prev.sum += obj.a; }';
  141 + $finalize = 'function(obj) { if (obj.count) { obj.avg = obj.sum / obj.count; } else { obj.avg = 0; } }';
  142 +
94 143 $qb = $this->getTestQueryBuilder()
95   - ->group(array(), array());
  144 + ->group($keys, $initial, $reduce, array('finalize' => $finalize));
  145 +
  146 + $expected = array(
  147 + 'keys' => $keys,
  148 + 'initial' => $initial,
  149 + 'reduce' => $reduce,
  150 + 'options' => array('finalize' => $finalize),
  151 + );
  152 +
  153 + $this->assertEquals(Query::TYPE_GROUP, $qb->getType());
  154 + $this->assertEquals($expected, $qb->debug('group'));
  155 + $this->assertInstanceOf('Doctrine\MongoDB\ArrayIterator', $qb->getQuery()->execute());
  156 + }
  157 +
  158 + public function testGroupQueryWithMultipleMethods()
  159 + {
  160 + $keys = array();
  161 + $initial = array('count' => 0, 'sum' => 0);
  162 + $reduce = 'function(obj, prev) { prev.count++; prev.sum += obj.a; }';
  163 + $finalize = 'function(obj) { if (obj.count) { obj.avg = obj.sum / obj.count; } else { obj.avg = 0; } }';
  164 +
  165 + $qb = $this->getTestQueryBuilder()
  166 + ->group($keys, $initial)
  167 + ->reduce($reduce)
  168 + ->finalize($finalize);
  169 +
  170 + $expected = array(
  171 + 'keys' => $keys,
  172 + 'initial' => $initial,
  173 + 'reduce' => $reduce,
  174 + 'options' => array('finalize' => $finalize),
  175 + );
96 176
97 177 $this->assertEquals(Query::TYPE_GROUP, $qb->getType());
  178 + $this->assertEquals($expected, $qb->debug('group'));
98 179 $this->assertInstanceOf('Doctrine\MongoDB\ArrayIterator', $qb->getQuery()->execute());
99 180 }
100 181
@@ -141,6 +222,22 @@ public function testRemoveQuery()
141 222 $this->assertTrue($qb->getQuery()->execute());
142 223 }
143 224
  225 + /**
  226 + * @expectedException BadMethodCallException
  227 + */
  228 + public function testFinalizeShouldThrowExceptionForUnsupportedQueryType()
  229 + {
  230 + $qb = $this->getTestQueryBuilder()->finalize('function() { }');
  231 + }
  232 +
  233 + /**
  234 + * @expectedException BadMethodCallException
  235 + */
  236 + public function testReduceShouldThrowExceptionForUnsupportedQueryType()
  237 + {
  238 + $qb = $this->getTestQueryBuilder()->reduce('function() { }');
  239 + }
  240 +
144 241 public function testThatOrAcceptsAnotherQuery()
145 242 {
146 243 $coll = $this->conn->selectCollection('db', 'users');
@@ -292,27 +389,6 @@ public function testUnsetField()
292 389 $this->assertEquals($expected, $qb->getNewObj());
293 390 }
294 391
295   - public function testGroup()
296   - {
297   - $qb = $this->getTestQueryBuilder()
298   - ->group(array(), array('count' => 0))
299   - ->reduce('function (obj, prev) { prev.count++; }');
300   -
301   - $expected = array(
302   - 'initial' => array(
303   - 'count' => 0
304   - ),
305   - 'keys' => array()
306   - );
307   - $this->assertEquals($expected, $qb->debug('group'));
308   -
309   - $expected = array(
310   - 'map' => null,
311   - 'options' => array(),
312   - 'reduce' => 'function (obj, prev) { prev.count++; }');
313   - $this->assertEquals($expected, $qb->debug('mapReduce'));
314   - }
315   -
316 392 public function testDateRange()
317 393 {
318 394 $start = new \MongoDate(strtotime('1985-09-01 01:00:00'));

0 comments on commit 17e9c5a

Please sign in to comment.
Something went wrong with that request. Please try again.