From 0e082c170cda0115683483d88ef108569e2ae482 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Mon, 2 Dec 2013 14:01:46 +0200 Subject: [PATCH] Aggregation functions added to Mongo Query. --- extensions/mongo/Collection.php | 28 ++++-- extensions/mongo/Query.php | 121 +++++++++++++++++++++-- tests/unit/extensions/mongo/ActiveRecordTest.php | 7 +- 3 files changed, 137 insertions(+), 19 deletions(-) diff --git a/extensions/mongo/Collection.php b/extensions/mongo/Collection.php index 7d3e2a5..daa3ff1 100644 --- a/extensions/mongo/Collection.php +++ b/extensions/mongo/Collection.php @@ -202,9 +202,11 @@ class Collection extends Object } /** - * @param $pipeline - * @param array $pipelineOperator - * @return array + * Performs aggregation using Mongo Aggregation Framework. + * @param array $pipeline list of pipeline operators, or just the first operator + * @param array $pipelineOperator Additional pipeline operators + * @return array the result of the aggregation. + * @see http://docs.mongodb.org/manual/applications/aggregation/ */ public function aggregate($pipeline, $pipelineOperator = []) { @@ -213,20 +215,30 @@ class Collection extends Object } /** + * Performs aggregation using Mongo Map Reduce mechanism. * @param mixed $keys - * @param array $initial - * @param \MongoCode|string $reduce - * @param array $options - * @return array + * @param array $initial Initial value of the aggregation counter object. + * @param \MongoCode|string $reduce function that takes two arguments (the current + * document and the aggregation to this point) and does the aggregation. + * Argument will be automatically cast to [[\MongoCode]]. + * @param array $options optional parameters to the group command. Valid options include: + * - condition - criteria for including a document in the aggregation. + * - finalize - function called once per unique key that takes the final output of the reduce function. + * @return array the result of the aggregation. */ public function mapReduce($keys, $initial, $reduce, $options = []) { if (!($reduce instanceof \MongoCode)) { - $reduce = new \MongoCode($reduce); + $reduce = new \MongoCode((string)$reduce); } if (array_key_exists('condition', $options)) { $options['condition'] = $this->buildCondition($options['condition']); } + if (array_key_exists('finalize', $options)) { + if (!($options['finalize'] instanceof \MongoCode)) { + $options['finalize'] = new \MongoCode((string)$options['finalize']); + } + } return $this->mongoCollection->group($keys, $initial, $reduce, $options); } diff --git a/extensions/mongo/Query.php b/extensions/mongo/Query.php index 13be87d..6259b99 100644 --- a/extensions/mongo/Query.php +++ b/extensions/mongo/Query.php @@ -105,8 +105,8 @@ class Query extends Component implements QueryInterface /** * Executes the query and returns all results as an array. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. + * @param Connection $db the Mongo connection used to execute the query. + * If this parameter is not given, the `mongo` application component will be used. * @return array the query results. If the query results in nothing, an empty array will be returned. */ public function all($db = null) @@ -130,8 +130,8 @@ class Query extends Component implements QueryInterface /** * Executes the query and returns a single row of result. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. + * @param Connection $db the Mongo connection used to execute the query. + * If this parameter is not given, the `mongo` application component will be used. * @return array|boolean the first row (in terms of an array) of the query result. False is returned if the query * results in nothing. */ @@ -148,8 +148,8 @@ class Query extends Component implements QueryInterface /** * Returns the number of records. * @param string $q the COUNT expression. Defaults to '*'. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. + * @param Connection $db the Mongo connection used to execute the query. + * If this parameter is not given, the `mongo` application component will be used. * @return integer number of records */ public function count($q = '*', $db = null) @@ -160,12 +160,117 @@ class Query extends Component implements QueryInterface /** * Returns a value indicating whether the query result contains any row of data. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `db` application component will be used. + * @param Connection $db the Mongo connection used to execute the query. + * If this parameter is not given, the `mongo` application component will be used. * @return boolean whether the query result contains any row of data. */ public function exists($db = null) { return $this->one($db) !== null; } + + /** + * Returns the sum of the specified column values. + * @param string $q the column name or expression. + * Make sure you properly quote column names in the expression. + * @param Connection $db the Mongo connection used to execute the query. + * If this parameter is not given, the `mongo` application component will be used. + * @return integer the sum of the specified column values + */ + public function sum($q, $db = null) + { + return $this->aggregate($q, 'sum', $db); + } + + /** + * Returns the average of the specified column values. + * @param string $q the column name or expression. + * Make sure you properly quote column names in the expression. + * @param Connection $db the Mongo connection used to execute the query. + * If this parameter is not given, the `mongo` application component will be used. + * @return integer the average of the specified column values. + */ + public function average($q, $db = null) + { + return $this->aggregate($q, 'avg', $db); + } + + /** + * Returns the minimum of the specified column values. + * @param string $q the column name or expression. + * Make sure you properly quote column names in the expression. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return integer the minimum of the specified column values. + */ + public function min($q, $db = null) + { + return $this->aggregate($q, 'min', $db); + } + + /** + * Returns the maximum of the specified column values. + * @param string $q the column name or expression. + * Make sure you properly quote column names in the expression. + * @param Connection $db the Mongo connection used to execute the query. + * If this parameter is not given, the `mongo` application component will be used. + * @return integer the maximum of the specified column values. + */ + public function max($q, $db = null) + { + return $this->aggregate($q, 'max', $db); + } + + /** + * Performs the aggregation for the given column. + * @param string $column column name. + * @param string $operator aggregation operator. + * @param Connection $db the database connection used to execute the query. + * @return integer aggregation result. + */ + protected function aggregate($column, $operator, $db) + { + $collection = $this->getCollection($db); + $pipelines = []; + if ($this->where !== null) { + $pipelines[] = ['$match' => $collection->buildCondition($this->where)]; + } + $pipelines[] = [ + '$group' => [ + '_id' => '1', + 'total' => [ + '$' . $operator => '$' . $column + ], + ] + ]; + $result = $collection->aggregate($pipelines); + if (!empty($result['ok'])) { + return $result['result'][0]['total']; + } else { + return 0; + } + } + + /** + * Returns a list of distinct values for the given column across a collection. + * @param string $q column to use. + * @param Connection $db the Mongo connection used to execute the query. + * If this parameter is not given, the `mongo` application component will be used. + * @return array array of distinct values + */ + public function distinct($q, $db = null) + { + $collection = $this->getCollection($db); + if ($this->where !== null) { + $condition = $this->where; + } else { + $condition = []; + } + $result = $collection->distinct($q, $condition); + if ($result === false) { + return []; + } else { + return $result; + } + } } \ No newline at end of file diff --git a/tests/unit/extensions/mongo/ActiveRecordTest.php b/tests/unit/extensions/mongo/ActiveRecordTest.php index e96ea30..812d6e3 100644 --- a/tests/unit/extensions/mongo/ActiveRecordTest.php +++ b/tests/unit/extensions/mongo/ActiveRecordTest.php @@ -83,13 +83,14 @@ class ActiveRecordTest extends MongoTestCase $this->assertTrue($customer instanceof Customer); $this->assertEquals(4, $customer->status); - // find count, sum, average, min, max, scalar + // find count, sum, average, min, max, distinct $this->assertEquals(10, Customer::find()->count()); $this->assertEquals(1, Customer::find()->where(['status' => 2])->count()); - /*$this->assertEquals((1+10)/2*10, Customer::find()->sum('status')); + $this->assertEquals((1+10)/2*10, Customer::find()->sum('status')); $this->assertEquals((1+10)/2, Customer::find()->average('status')); $this->assertEquals(1, Customer::find()->min('status')); - $this->assertEquals(10, Customer::find()->max('status'));*/ + $this->assertEquals(10, Customer::find()->max('status')); + $this->assertEquals(range(1, 10), Customer::find()->distinct('status')); // scope $this->assertEquals(1, Customer::find()->activeOnly()->count());