<commit_before><?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

namespace yii\mongodb;

use yii\base\InvalidParamException;
use yii\base\Object;
use Yii;
use yii\helpers\Json;

/**
 * Collection represents the Mongo collection information.
 *
 * A collection object is usually created by calling [[Database::getCollection()]] or [[Connection::getCollection()]].
 *
 * Collection provides the basic interface for the Mongo queries, mostly: insert, update, delete operations.
 * For example:
 *
 * ~~~
 * $collection = Yii::$app->mongodb->getCollection('customer');
 * $collection->insert(['name' => '<NAME>', 'status' => 1]);
 * ~~~
 *
 * To perform "find" queries, please use [[Query]] instead.
 *
 * Mongo uses JSON format to specify query conditions with quite specific syntax.
 * However Collection class provides the ability of "translating" common condition format used "yii\db\*"
 * into Mongo condition.
 * For example:
 * ~~~
 * $condition = [
 *     [
 *         'OR',
 *         ['AND', ['first_name' => 'John'], ['last_name' => 'Smith']],
 *         ['status' => [1, 2, 3]]
 *     ],
 * ];
 * print_r($collection->buildCondition($condition));
 * // outputs :
 * [
 *     '$or' => [
 *         [
 *             'first_name' => 'John',
 *             'last_name' => 'John',
 *         ],
 *         [
 *             'status' => ['$in' => [1, 2, 3]],
 *         ]
 *     ]
 * ]
 * ~~~
 *
 * Note: condition values for the key '_id' will be automatically cast to [[\MongoId]] instance,
 * even if they are plain strings. However, if you have other columns, containing [[\MongoId]], you
 * should take care of possible typecast on your own.
 *
 * @property string $fullName Full name of this collection, including database name. This property is
 * read-only.
 * @property array $lastError Last error information. This property is read-only.
 * @property string $name Name of this collection. This property is read-only.
 *
 * @author <NAME> <<EMAIL>>
 * @since 2.0
 */
class Collection extends Object
{
	/**
	 * @var \MongoCollection Mongo collection instance.
	 */
	public $mongoCollection;

	/**
	 * @return string name of this collection.
	 */
	public function getName()
	{
		return $this->mongoCollection->getName();
	}

	/**
	 * @return string full name of this collection, including database name.
	 */
	public function getFullName()
	{
		return $this->mongoCollection->__toString();
	}

	/**
	 * @return array last error information.
	 */
	public function getLastError()
	{
		return $this->mongoCollection->db->lastError();
	}

	/**
	 * Composes log/profile token.
	 * @param string $command command name
	 * @param array $arguments command arguments.
	 * @return string token.
	 */
	protected function composeLogToken($command, $arguments = [])
	{
		$parts = [];
		foreach ($arguments as $argument) {
			$parts[] = is_scalar($argument) ? $argument : Json::encode($argument);
		}
		return $this->getFullName() . '.' . $command . '(' . implode(', ', $parts) . ')';
	}

	/**
	 * Drops this collection.
	 * @throws Exception on failure.
	 * @return boolean whether the operation successful.
	 */
	public function drop()
	{
		$token = $this->composeLogToken('drop');
		Yii::info($token, __METHOD__);
		try {
			Yii::beginProfile($token, __METHOD__);
			$result = $this->mongoCollection->drop();
			$this->tryResultError($result);
			Yii::endProfile($token, __METHOD__);
			return true;
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Creates an index on the collection and the specified fields.
	 * @param array|string $columns column name or list of column names.
	 * If array is given, each element in the array has as key the field name, and as
	 * value either 1 for ascending sort, or -1 for descending sort.
	 * You can specify field using native numeric key with the field name as a value,
	 * in this case ascending sort will be used.
	 * For example:
	 * ~~~
	 * [
	 *     'name',
	 *     'status' => -1,
	 * ]
	 * ~~~
	 * @param array $options list of options in format: optionName => optionValue.
	 * @throws Exception on failure.
	 * @return boolean whether the operation successful.
	 */
	public function createIndex($columns, $options = [])
	{
		if (!is_array($columns)) {
			$columns = [$columns];
		}
		$keys = $this->normalizeIndexKeys($columns);
		$token = $this->composeLogToken('createIndex', [$keys, $options]);
		$options = array_merge(['w' => 1], $options);
		Yii::info($token, __METHOD__);
		try {
			Yii::beginProfile($token, __METHOD__);
			$result = $this->mongoCollection->ensureIndex($keys, $options);
			$this->tryResultError($result);
			Yii::endProfile($token, __METHOD__);
			return true;
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Drop indexes for specified column(s).
	 * @param string|array $columns column name or list of column names.
	 * If array is given, each element in the array has as key the field name, and as
	 * value either 1 for ascending sort, or -1 for descending sort.
	 * Use value 'text' to specify text index.
	 * You can specify field using native numeric key with the field name as a value,
	 * in this case ascending sort will be used.
	 * For example:
	 * ~~~
	 * [
	 *     'name',
	 *     'status' => -1,
	 *     'description' => 'text',
	 * ]
	 * ~~~
	 * @throws Exception on failure.
	 * @return boolean whether the operation successful.
	 */
	public function dropIndex($columns)
	{
		if (!is_array($columns)) {
			$columns = [$columns];
		}
		$keys = $this->normalizeIndexKeys($columns);
		$token = $this->composeLogToken('dropIndex', [$keys]);
		Yii::info($token, __METHOD__);
		try {
			$result = $this->mongoCollection->deleteIndex($keys);
			$this->tryResultError($result);
			return true;
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Compose index keys from given columns/keys list.
	 * @param array $columns raw columns/keys list.
	 * @return array normalizes index keys array.
	 */
	protected function normalizeIndexKeys($columns)
	{
		$keys = [];
		foreach ($columns as $key => $value) {
			if (is_numeric($key)) {
				$keys[$value] = \MongoCollection::ASCENDING;
			} else {
				$keys[$key] = $value;
			}
		}
		return $keys;
	}

	/**
	 * Drops all indexes for this collection.
	 * @throws Exception on failure.
	 * @return integer count of dropped indexes.
	 */
	public function dropAllIndexes()
	{
		$token = $this->composeLogToken('dropIndexes');
		Yii::info($token, __METHOD__);
		try {
			$result = $this->mongoCollection->deleteIndexes();
			$this->tryResultError($result);
			return $result['nIndexesWas'];
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Returns a cursor for the search results.
	 * In order to perform "find" queries use [[Query]] class.
	 * @param array $condition query condition
	 * @param array $fields fields to be selected
	 * @return \MongoCursor cursor for the search results
	 * @see Query
	 */
	public function find($condition = [], $fields = [])
	{
		return $this->mongoCollection->find($this->buildCondition($condition), $fields);
	}

	/**
	 * Inserts new data into collection.
	 * @param array|object $data data to be inserted.
	 * @param array $options list of options in format: optionName => optionValue.
	 * @return \MongoId new record id instance.
	 * @throws Exception on failure.
	 */
	public function insert($data, $options = [])
	{
		$token = $this->composeLogToken('insert', [$data]);
		Yii::info($token, __METHOD__);
		try {
			Yii::beginProfile($token, __METHOD__);
			$options = array_merge(['w' => 1], $options);
			$this->tryResultError($this->mongoCollection->insert($data, $options));
			Yii::endProfile($token, __METHOD__);
			return is_array($data) ? $data['_id'] : $data->_id;
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Inserts several new rows into collection.
	 * @param array $rows array of arrays or objects to be inserted.
	 * @param array $options list of options in format: optionName => optionValue.
	 * @return array inserted data, each row will have "_id" key assigned to it.
	 * @throws Exception on failure.
	 */
	public function batchInsert($rows, $options = [])
	{
		$token = $this->composeLogToken('batchInsert', [$rows]);
		Yii::info($token, __METHOD__);
		try {
			Yii::beginProfile($token, __METHOD__);
			$options = array_merge(['w' => 1], $options);
			$this->tryResultError($this->mongoCollection->batchInsert($rows, $options));
			Yii::endProfile($token, __METHOD__);
			return $rows;
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Updates the rows, which matches given criteria by given data.
	 * Note: for "multiple" mode Mongo requires explicit strategy "$set" or "$inc"
	 * to be specified for the "newData". If no strategy is passed "$set" will be used.
	 * @param array $condition description of the objects to update.
	 * @param array $newData the object with which to update the matching records.
	 * @param array $options list of options in format: optionName => optionValue.
	 * @return integer|boolean number of updated documents or whether operation was successful.
	 * @throws Exception on failure.
	 */
	public function update($condition, $newData, $options = [])
	{
		$condition = $this->buildCondition($condition);
		$options = array_merge(['w' => 1, 'multiple' => true], $options);
		if ($options['multiple']) {
			$keys = array_keys($newData);
			if (!empty($keys) && strncmp('$', $keys[0], 1) !== 0) {
				$newData = ['$set' => $newData];
			}
		}
		$token = $this->composeLogToken('update', [$condition, $newData, $options]);
		Yii::info($token, __METHOD__);
		try {
			Yii::beginProfile($token, __METHOD__);
			$result = $this->mongoCollection->update($condition, $newData, $options);
			$this->tryResultError($result);
			Yii::endProfile($token, __METHOD__);
			if (is_array($result) && array_key_exists('n', $result)) {
				return $result['n'];
			} else {
				return true;
			}
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Update the existing database data, otherwise insert this data
	 * @param array|object $data data to be updated/inserted.
	 * @param array $options list of options in format: optionName => optionValue.
	 * @return \MongoId updated/new record id instance.
	 * @throws Exception on failure.
	 */
	public function save($data, $options = [])
	{
		$token = $this->composeLogToken('save', [$data]);
		Yii::info($token, __METHOD__);
		try {
			Yii::beginProfile($token, __METHOD__);
			$options = array_merge(['w' => 1], $options);
			$this->tryResultError($this->mongoCollection->save($data, $options));
			Yii::endProfile($token, __METHOD__);
			return is_array($data) ? $data['_id'] : $data->_id;
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Removes data from the collection.
	 * @param array $condition description of records to remove.
	 * @param array $options list of options in format: optionName => optionValue.
	 * @return integer|boolean number of updated documents or whether operation was successful.
	 * @throws Exception on failure.
	 */
	public function remove($condition = [], $options = [])
	{
		$condition = $this->buildCondition($condition);
		$options = array_merge(['w' => 1, 'multiple' => true], $options);
		$token = $this->composeLogToken('remove', [$condition, $options]);
		Yii::info($token, __METHOD__);
		try {
			Yii::beginProfile($token, __METHOD__);
			$result = $this->mongoCollection->remove($condition, $options);
			$this->tryResultError($result);
			Yii::endProfile($token, __METHOD__);
			if (is_array($result) && array_key_exists('n', $result)) {
				return $result['n'];
			} else {
				return true;
			}
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Returns a list of distinct values for the given column across a collection.
	 * @param string $column column to use.
	 * @param array $condition query parameters.
	 * @return array|boolean array of distinct values, or "false" on failure.
	 * @throws Exception on failure.
	 */
	public function distinct($column, $condition = [])
	{
		$condition = $this->buildCondition($condition);
		$token = $this->composeLogToken('distinct', [$column, $condition]);
		Yii::info($token, __METHOD__);
		try {
			Yii::beginProfile($token, __METHOD__);
			$result = $this->mongoCollection->distinct($column, $condition);
			Yii::endProfile($token, __METHOD__);
			return $result;
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Performs aggregation using Mongo Aggregation Framework.
	 * @param array $pipeline list of pipeline operators, or just the first operator
	 * @param array $pipelineOperator additional pipeline operator. You can specify additional
	 * pipelines via third argument, fourth argument etc.
	 * @return array the result of the aggregation.
	 * @throws Exception on failure.
	 * @see http://docs.mongodb.org/manual/applications/aggregation/
	 */
	public function aggregate($pipeline, $pipelineOperator = [])
	{
		$args = func_get_args();
		$token = $this->composeLogToken('aggregate', $args);
		Yii::info($token, __METHOD__);
		try {
			Yii::beginProfile($token, __METHOD__);
			$result = call_user_func_array([$this->mongoCollection, 'aggregate'], $args);
			$this->tryResultError($result);
			Yii::endProfile($token, __METHOD__);
			return $result['result'];
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Performs aggregation using Mongo "group" command.
	 * @param mixed $keys fields to group by. If an array or non-code object is passed,
	 * it will be the key used to group results. If instance of [[\MongoCode]] passed,
	 * it will be treated as a function that returns the key to group by.
	 * @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.
	 * @throws Exception on failure.
	 * @see http://docs.mongodb.org/manual/reference/command/group/
	 */
	public function group($keys, $initial, $reduce, $options = [])
	{
		if (!($reduce instanceof \MongoCode)) {
			$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']);
			}
		}
		$token = $this->composeLogToken('group', [$keys, $initial, $reduce, $options]);
		Yii::info($token, __METHOD__);
		try {
			Yii::beginProfile($token, __METHOD__);
			// Avoid possible E_DEPRECATED for $options:
			if (empty($options)) {
				$result = $this->mongoCollection->group($keys, $initial, $reduce);
			} else {
				$result = $this->mongoCollection->group($keys, $initial, $reduce, $options);
			}
			$this->tryResultError($result);

			Yii::endProfile($token, __METHOD__);
			if (array_key_exists('retval', $result)) {
				return $result['retval'];
			} else {
				return [];
			}
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Performs aggregation using Mongo "map reduce" mechanism.
	 * Note: this function will not return the aggregation result, instead it will
	 * write it inside the another Mongo collection specified by "out" parameter.
	 * For example:
	 *
	 * ~~~
	 * $customerCollection = Yii::$app->mongo->getCollection('customer');
	 * $resultCollectionName = $customerCollection->mapReduce(
	 *     'function () {emit(this.status, this.amount)}',
	 *     'function (key, values) {return Array.sum(values)}',
	 *     'mapReduceOut',
	 *     ['status' => 3]
	 * );
	 * $query = new Query();
	 * $results = $query->from($resultCollectionName)->all();
	 * ~~~
	 *
	 * @param \MongoCode|string $map function, which emits map data from collection.
	 * Argument will be automatically cast to [[\MongoCode]].
	 * @param \MongoCode|string $reduce function that takes two arguments (the map key
	 * and the map values) and does the aggregation.
	 * Argument will be automatically cast to [[\MongoCode]].
	 * @param string|array $out output collection name. It could be a string for simple output
	 * ('outputCollection'), or an array for parametrized output (['merge' => 'outputCollection'])
	 * @param array $condition criteria for including a document in the aggregation.
	 * @param array $options additional optional parameters to the mapReduce command. Valid options include:
	 *  - sort - array - key to sort the input documents. The sort key must be in an existing index for this collection.
	 *  - limit - the maximum number of documents to return in the collection.
	 *  - finalize - function, which follows the reduce method and modifies the output.
	 *  - scope - array - specifies global variables that are accessible in the map, reduce and finalize functions.
	 *  - jsMode - boolean -Specifies whether to convert intermediate data into BSON format between the execution of the map and reduce functions.
	 *  - verbose - boolean - specifies whether to include the timing information in the result information.
	 * @return string the map reduce output collection name.
	 * @throws Exception on failure.
	 */
	public function mapReduce($map, $reduce, $out, $condition = [], $options = [])
	{
		if (!($map instanceof \MongoCode)) {
			$map = new \MongoCode((string)$map);
		}
		if (!($reduce instanceof \MongoCode)) {
			$reduce = new \MongoCode((string)$reduce);
		}
		$command = [
			'mapReduce' => $this->getName(),
			'map' => $map,
			'reduce' => $reduce,
			'out' => $out
		];
		if (!empty($condition)) {
			$command['query'] = $this->buildCondition($condition);
		}
		if (array_key_exists('finalize', $options)) {
			if (!($options['finalize'] instanceof \MongoCode)) {
				$options['finalize'] = new \MongoCode((string)$options['finalize']);
			}
		}
		if (!empty($options)) {
			$command = array_merge($command, $options);
		}
		$token = $this->composeLogToken('mapReduce', [$map, $reduce, $out]);
		Yii::info($token, __METHOD__);
		try {
			Yii::beginProfile($token, __METHOD__);
			$command = array_merge(['mapReduce' => $this->getName()], $command);
			$result = $this->mongoCollection->db->command($command);
			$this->tryResultError($result);
			Yii::endProfile($token, __METHOD__);
			return $result['result'];
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Performs full text search.
	 * @param string $search string of terms that MongoDB parses and uses to query the text index.
	 * @param array $condition criteria for filtering a results list.
	 * @param array $fields list of fields to be returned in result.
	 * @param array $options additional optional parameters to the mapReduce command. Valid options include:
	 *  - limit - the maximum number of documents to include in the response (by default 100).
	 *  - language - the language that determines the list of stop words for the search
	 * and the rules for the stemmer and tokenizer. If not specified, the search uses the default
	 * language of the index.
	 * @return array the highest scoring documents, in descending order by score.
	 * @throws Exception on failure.
	 */
	public function fullTextSearch($search, $condition = [], $fields = [], $options = []) {
		$command = [
			'search' => $search
		];
		if (!empty($condition)) {
			$command['filter'] = $this->buildCondition($condition);
		}
		if (!empty($fields)) {
			$command['project'] = $fields;
		}<commit_msg>yii\mongodb\Collection::mapReduce() fixed to handle 'inline' output correctly.<commit_after><?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

namespace yii\mongodb;

use yii\base\InvalidParamException;
use yii\base\Object;
use Yii;
use yii\helpers\Json;

/**
 * Collection represents the Mongo collection information.
 *
 * A collection object is usually created by calling [[Database::getCollection()]] or [[Connection::getCollection()]].
 *
 * Collection provides the basic interface for the Mongo queries, mostly: insert, update, delete operations.
 * For example:
 *
 * ~~~
 * $collection = Yii::$app->mongodb->getCollection('customer');
 * $collection->insert(['name' => '<NAME>', 'status' => 1]);
 * ~~~
 *
 * To perform "find" queries, please use [[Query]] instead.
 *
 * Mongo uses JSON format to specify query conditions with quite specific syntax.
 * However Collection class provides the ability of "translating" common condition format used "yii\db\*"
 * into Mongo condition.
 * For example:
 * ~~~
 * $condition = [
 *     [
 *         'OR',
 *         ['AND', ['first_name' => 'John'], ['last_name' => 'Smith']],
 *         ['status' => [1, 2, 3]]
 *     ],
 * ];
 * print_r($collection->buildCondition($condition));
 * // outputs :
 * [
 *     '$or' => [
 *         [
 *             'first_name' => 'John',
 *             'last_name' => 'John',
 *         ],
 *         [
 *             'status' => ['$in' => [1, 2, 3]],
 *         ]
 *     ]
 * ]
 * ~~~
 *
 * Note: condition values for the key '_id' will be automatically cast to [[\MongoId]] instance,
 * even if they are plain strings. However, if you have other columns, containing [[\MongoId]], you
 * should take care of possible typecast on your own.
 *
 * @property string $fullName Full name of this collection, including database name. This property is
 * read-only.
 * @property array $lastError Last error information. This property is read-only.
 * @property string $name Name of this collection. This property is read-only.
 *
 * @author <NAME> <<EMAIL>>
 * @since 2.0
 */
class Collection extends Object
{
	/**
	 * @var \MongoCollection Mongo collection instance.
	 */
	public $mongoCollection;

	/**
	 * @return string name of this collection.
	 */
	public function getName()
	{
		return $this->mongoCollection->getName();
	}

	/**
	 * @return string full name of this collection, including database name.
	 */
	public function getFullName()
	{
		return $this->mongoCollection->__toString();
	}

	/**
	 * @return array last error information.
	 */
	public function getLastError()
	{
		return $this->mongoCollection->db->lastError();
	}

	/**
	 * Composes log/profile token.
	 * @param string $command command name
	 * @param array $arguments command arguments.
	 * @return string token.
	 */
	protected function composeLogToken($command, $arguments = [])
	{
		$parts = [];
		foreach ($arguments as $argument) {
			$parts[] = is_scalar($argument) ? $argument : Json::encode($argument);
		}
		return $this->getFullName() . '.' . $command . '(' . implode(', ', $parts) . ')';
	}

	/**
	 * Drops this collection.
	 * @throws Exception on failure.
	 * @return boolean whether the operation successful.
	 */
	public function drop()
	{
		$token = $this->composeLogToken('drop');
		Yii::info($token, __METHOD__);
		try {
			Yii::beginProfile($token, __METHOD__);
			$result = $this->mongoCollection->drop();
			$this->tryResultError($result);
			Yii::endProfile($token, __METHOD__);
			return true;
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Creates an index on the collection and the specified fields.
	 * @param array|string $columns column name or list of column names.
	 * If array is given, each element in the array has as key the field name, and as
	 * value either 1 for ascending sort, or -1 for descending sort.
	 * You can specify field using native numeric key with the field name as a value,
	 * in this case ascending sort will be used.
	 * For example:
	 * ~~~
	 * [
	 *     'name',
	 *     'status' => -1,
	 * ]
	 * ~~~
	 * @param array $options list of options in format: optionName => optionValue.
	 * @throws Exception on failure.
	 * @return boolean whether the operation successful.
	 */
	public function createIndex($columns, $options = [])
	{
		if (!is_array($columns)) {
			$columns = [$columns];
		}
		$keys = $this->normalizeIndexKeys($columns);
		$token = $this->composeLogToken('createIndex', [$keys, $options]);
		$options = array_merge(['w' => 1], $options);
		Yii::info($token, __METHOD__);
		try {
			Yii::beginProfile($token, __METHOD__);
			$result = $this->mongoCollection->ensureIndex($keys, $options);
			$this->tryResultError($result);
			Yii::endProfile($token, __METHOD__);
			return true;
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Drop indexes for specified column(s).
	 * @param string|array $columns column name or list of column names.
	 * If array is given, each element in the array has as key the field name, and as
	 * value either 1 for ascending sort, or -1 for descending sort.
	 * Use value 'text' to specify text index.
	 * You can specify field using native numeric key with the field name as a value,
	 * in this case ascending sort will be used.
	 * For example:
	 * ~~~
	 * [
	 *     'name',
	 *     'status' => -1,
	 *     'description' => 'text',
	 * ]
	 * ~~~
	 * @throws Exception on failure.
	 * @return boolean whether the operation successful.
	 */
	public function dropIndex($columns)
	{
		if (!is_array($columns)) {
			$columns = [$columns];
		}
		$keys = $this->normalizeIndexKeys($columns);
		$token = $this->composeLogToken('dropIndex', [$keys]);
		Yii::info($token, __METHOD__);
		try {
			$result = $this->mongoCollection->deleteIndex($keys);
			$this->tryResultError($result);
			return true;
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Compose index keys from given columns/keys list.
	 * @param array $columns raw columns/keys list.
	 * @return array normalizes index keys array.
	 */
	protected function normalizeIndexKeys($columns)
	{
		$keys = [];
		foreach ($columns as $key => $value) {
			if (is_numeric($key)) {
				$keys[$value] = \MongoCollection::ASCENDING;
			} else {
				$keys[$key] = $value;
			}
		}
		return $keys;
	}

	/**
	 * Drops all indexes for this collection.
	 * @throws Exception on failure.
	 * @return integer count of dropped indexes.
	 */
	public function dropAllIndexes()
	{
		$token = $this->composeLogToken('dropIndexes');
		Yii::info($token, __METHOD__);
		try {
			$result = $this->mongoCollection->deleteIndexes();
			$this->tryResultError($result);
			return $result['nIndexesWas'];
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Returns a cursor for the search results.
	 * In order to perform "find" queries use [[Query]] class.
	 * @param array $condition query condition
	 * @param array $fields fields to be selected
	 * @return \MongoCursor cursor for the search results
	 * @see Query
	 */
	public function find($condition = [], $fields = [])
	{
		return $this->mongoCollection->find($this->buildCondition($condition), $fields);
	}

	/**
	 * Inserts new data into collection.
	 * @param array|object $data data to be inserted.
	 * @param array $options list of options in format: optionName => optionValue.
	 * @return \MongoId new record id instance.
	 * @throws Exception on failure.
	 */
	public function insert($data, $options = [])
	{
		$token = $this->composeLogToken('insert', [$data]);
		Yii::info($token, __METHOD__);
		try {
			Yii::beginProfile($token, __METHOD__);
			$options = array_merge(['w' => 1], $options);
			$this->tryResultError($this->mongoCollection->insert($data, $options));
			Yii::endProfile($token, __METHOD__);
			return is_array($data) ? $data['_id'] : $data->_id;
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Inserts several new rows into collection.
	 * @param array $rows array of arrays or objects to be inserted.
	 * @param array $options list of options in format: optionName => optionValue.
	 * @return array inserted data, each row will have "_id" key assigned to it.
	 * @throws Exception on failure.
	 */
	public function batchInsert($rows, $options = [])
	{
		$token = $this->composeLogToken('batchInsert', [$rows]);
		Yii::info($token, __METHOD__);
		try {
			Yii::beginProfile($token, __METHOD__);
			$options = array_merge(['w' => 1], $options);
			$this->tryResultError($this->mongoCollection->batchInsert($rows, $options));
			Yii::endProfile($token, __METHOD__);
			return $rows;
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Updates the rows, which matches given criteria by given data.
	 * Note: for "multiple" mode Mongo requires explicit strategy "$set" or "$inc"
	 * to be specified for the "newData". If no strategy is passed "$set" will be used.
	 * @param array $condition description of the objects to update.
	 * @param array $newData the object with which to update the matching records.
	 * @param array $options list of options in format: optionName => optionValue.
	 * @return integer|boolean number of updated documents or whether operation was successful.
	 * @throws Exception on failure.
	 */
	public function update($condition, $newData, $options = [])
	{
		$condition = $this->buildCondition($condition);
		$options = array_merge(['w' => 1, 'multiple' => true], $options);
		if ($options['multiple']) {
			$keys = array_keys($newData);
			if (!empty($keys) && strncmp('$', $keys[0], 1) !== 0) {
				$newData = ['$set' => $newData];
			}
		}
		$token = $this->composeLogToken('update', [$condition, $newData, $options]);
		Yii::info($token, __METHOD__);
		try {
			Yii::beginProfile($token, __METHOD__);
			$result = $this->mongoCollection->update($condition, $newData, $options);
			$this->tryResultError($result);
			Yii::endProfile($token, __METHOD__);
			if (is_array($result) && array_key_exists('n', $result)) {
				return $result['n'];
			} else {
				return true;
			}
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Update the existing database data, otherwise insert this data
	 * @param array|object $data data to be updated/inserted.
	 * @param array $options list of options in format: optionName => optionValue.
	 * @return \MongoId updated/new record id instance.
	 * @throws Exception on failure.
	 */
	public function save($data, $options = [])
	{
		$token = $this->composeLogToken('save', [$data]);
		Yii::info($token, __METHOD__);
		try {
			Yii::beginProfile($token, __METHOD__);
			$options = array_merge(['w' => 1], $options);
			$this->tryResultError($this->mongoCollection->save($data, $options));
			Yii::endProfile($token, __METHOD__);
			return is_array($data) ? $data['_id'] : $data->_id;
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Removes data from the collection.
	 * @param array $condition description of records to remove.
	 * @param array $options list of options in format: optionName => optionValue.
	 * @return integer|boolean number of updated documents or whether operation was successful.
	 * @throws Exception on failure.
	 */
	public function remove($condition = [], $options = [])
	{
		$condition = $this->buildCondition($condition);
		$options = array_merge(['w' => 1, 'multiple' => true], $options);
		$token = $this->composeLogToken('remove', [$condition, $options]);
		Yii::info($token, __METHOD__);
		try {
			Yii::beginProfile($token, __METHOD__);
			$result = $this->mongoCollection->remove($condition, $options);
			$this->tryResultError($result);
			Yii::endProfile($token, __METHOD__);
			if (is_array($result) && array_key_exists('n', $result)) {
				return $result['n'];
			} else {
				return true;
			}
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Returns a list of distinct values for the given column across a collection.
	 * @param string $column column to use.
	 * @param array $condition query parameters.
	 * @return array|boolean array of distinct values, or "false" on failure.
	 * @throws Exception on failure.
	 */
	public function distinct($column, $condition = [])
	{
		$condition = $this->buildCondition($condition);
		$token = $this->composeLogToken('distinct', [$column, $condition]);
		Yii::info($token, __METHOD__);
		try {
			Yii::beginProfile($token, __METHOD__);
			$result = $this->mongoCollection->distinct($column, $condition);
			Yii::endProfile($token, __METHOD__);
			return $result;
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Performs aggregation using Mongo Aggregation Framework.
	 * @param array $pipeline list of pipeline operators, or just the first operator
	 * @param array $pipelineOperator additional pipeline operator. You can specify additional
	 * pipelines via third argument, fourth argument etc.
	 * @return array the result of the aggregation.
	 * @throws Exception on failure.
	 * @see http://docs.mongodb.org/manual/applications/aggregation/
	 */
	public function aggregate($pipeline, $pipelineOperator = [])
	{
		$args = func_get_args();
		$token = $this->composeLogToken('aggregate', $args);
		Yii::info($token, __METHOD__);
		try {
			Yii::beginProfile($token, __METHOD__);
			$result = call_user_func_array([$this->mongoCollection, 'aggregate'], $args);
			$this->tryResultError($result);
			Yii::endProfile($token, __METHOD__);
			return $result['result'];
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Performs aggregation using Mongo "group" command.
	 * @param mixed $keys fields to group by. If an array or non-code object is passed,
	 * it will be the key used to group results. If instance of [[\MongoCode]] passed,
	 * it will be treated as a function that returns the key to group by.
	 * @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.
	 * @throws Exception on failure.
	 * @see http://docs.mongodb.org/manual/reference/command/group/
	 */
	public function group($keys, $initial, $reduce, $options = [])
	{
		if (!($reduce instanceof \MongoCode)) {
			$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']);
			}
		}
		$token = $this->composeLogToken('group', [$keys, $initial, $reduce, $options]);
		Yii::info($token, __METHOD__);
		try {
			Yii::beginProfile($token, __METHOD__);
			// Avoid possible E_DEPRECATED for $options:
			if (empty($options)) {
				$result = $this->mongoCollection->group($keys, $initial, $reduce);
			} else {
				$result = $this->mongoCollection->group($keys, $initial, $reduce, $options);
			}
			$this->tryResultError($result);

			Yii::endProfile($token, __METHOD__);
			if (array_key_exists('retval', $result)) {
				return $result['retval'];
			} else {
				return [];
			}
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Performs aggregation using Mongo "map reduce" mechanism.
	 * Note: this function will not return the aggregation result, instead it will
	 * write it inside the another Mongo collection specified by "out" parameter.
	 * For example:
	 *
	 * ~~~
	 * $customerCollection = Yii::$app->mongo->getCollection('customer');
	 * $resultCollectionName = $customerCollection->mapReduce(
	 *     'function () {emit(this.status, this.amount)}',
	 *     'function (key, values) {return Array.sum(values)}',
	 *     'mapReduceOut',
	 *     ['status' => 3]
	 * );
	 * $query = new Query();
	 * $results = $query->from($resultCollectionName)->all();
	 * ~~~
	 *
	 * @param \MongoCode|string $map function, which emits map data from collection.
	 * Argument will be automatically cast to [[\MongoCode]].
	 * @param \MongoCode|string $reduce function that takes two arguments (the map key
	 * and the map values) and does the aggregation.
	 * Argument will be automatically cast to [[\MongoCode]].
	 * @param string|array $out output collection name. It could be a string for simple output
	 * ('outputCollection'), or an array for parametrized output (['merge' => 'outputCollection']).
     * You can pass ['inline' => true] to fetch the result at once without temporary collection usage.
	 * @param array $condition criteria for including a document in the aggregation.
	 * @param array $options additional optional parameters to the mapReduce command. Valid options include:
	 *  - sort - array - key to sort the input documents. The sort key must be in an existing index for this collection.
	 *  - limit - the maximum number of documents to return in the collection.
	 *  - finalize - function, which follows the reduce method and modifies the output.
	 *  - scope - array - specifies global variables that are accessible in the map, reduce and finalize functions.
	 *  - jsMode - boolean -Specifies whether to convert intermediate data into BSON format between the execution of the map and reduce functions.
	 *  - verbose - boolean - specifies whether to include the timing information in the result information.
	 * @return string|array the map reduce output collection name or output results.
	 * @throws Exception on failure.
	 */
	public function mapReduce($map, $reduce, $out, $condition = [], $options = [])
	{
		if (!($map instanceof \MongoCode)) {
			$map = new \MongoCode((string)$map);
		}
		if (!($reduce instanceof \MongoCode)) {
			$reduce = new \MongoCode((string)$reduce);
		}
		$command = [
			'mapReduce' => $this->getName(),
			'map' => $map,
			'reduce' => $reduce,
			'out' => $out
		];
		if (!empty($condition)) {
			$command['query'] = $this->buildCondition($condition);
		}
		if (array_key_exists('finalize', $options)) {
			if (!($options['finalize'] instanceof \MongoCode)) {
				$options['finalize'] = new \MongoCode((string)$options['finalize']);
			}
		}
		if (!empty($options)) {
			$command = array_merge($command, $options);
		}
		$token = $this->composeLogToken('mapReduce', [$map, $reduce, $out]);
		Yii::info($token, __METHOD__);
		try {
			Yii::beginProfile($token, __METHOD__);
			$command = array_merge(['mapReduce' => $this->getName()], $command);
			$result = $this->mongoCollection->db->command($command);
			$this->tryResultError($result);
			Yii::endProfile($token, __METHOD__);
			return array_key_exists('results', $result) ? $result['results'] : $result['result'];
		} catch (\Exception $e) {
			Yii::endProfile($token, __METHOD__);
			throw new Exception($e->getMessage(), (int)$e->getCode(), $e);
		}
	}

	/**
	 * Performs full text search.
	 * @param string $search string of terms that MongoDB parses and uses to query the text index.
	 * @param array $condition criteria for filtering a results list.
	 * @param array $fields list of fields to be returned in result.
	 * @param array $options additional optional parameters to the mapReduce command. Valid options include:
	 *  - limit - the maximum number of documents to include in the response (by default 100).
	 *  - language - the language that determines the list of stop words for the search
	 * and the rules for the stemmer and tokenizer. If not specified, the search uses the default
	 * language of the index.
	 * @return array the highest scoring documents, in descending order by score.
	 * @throws Exception on failure.
	 */
	public function fullTextSearch($search, $condition = [], $fields = [], $options = []) {
		$command = [
			'search' => $search
		];
		if (!empty($condition)) {
			$command['filter'] = $this->buildCondition($condition);
		}
		if (!empty($fields)) {
			$command['project'] = $fields;
		}
{'fraction_non_alphanumeric': 0.15308221607434205, 'fraction_numerical': 0.0015133243479700173, 'mean_word_length': 3.892642295233688, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 308, 'https://': 0, 'lorem ipsum': 0, 'www.': 4, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7506361', 'n_tokens_mistral': 13710, 'n_tokens_neox': 12473, 'n_words': 3945}