Skip to content
This repository has been archived by the owner on Nov 11, 2020. It is now read-only.

Added areFieldsIndexed method and Tests for it #154

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions lib/Doctrine/MongoDB/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,118 @@ public function isFieldIndexed($fieldName)
}
return false;
}

/**
* Check if given fields are prefix of any existing index. Fields'
* order does NOT matter in this method.
*
* http://docs.mongodb.org/manual/core/index-compound/#prefixes
* Given the following index: { "item": 1, "location": 1, "stock": 1 }
* Supported queries are those including
* * item
* * item and location
* * item and location and stock
* * item and stock (however, this index would be less efficient than an index on only item and stock)
*
* @param array $fieldsNames
* @param boolean $allowLessEfficient
* @return boolean
*/
public function areFieldsIndexed($fieldsNames, $allowLessEfficient = true)
{
$indexes = $this->getIndexInfo();
$numFields = count($fieldsNames);
foreach ($indexes as $index) {
// no keys or less keys are indexed than we need
if (!isset($index['key']) || count($index['key']) < $numFields) {
continue;
}
// array of index_field => position
$indexFieldPositions = array(); $i = 0;
foreach ($index['key'] as $field => $order) {
$indexFieldPositions[$field] = $i++;
}
$matchedPositions = array();
foreach ($fieldsNames as $field) {
if (isset($indexFieldPositions[$field])) {
$matchedPositions[] = $indexFieldPositions[$field];
} else {
continue 2; // field is not indexed, see next index
}
}
sort($matchedPositions);
if ($allowLessEfficient && $matchedPositions[0] === 0) {
return true;
}
foreach ($matchedPositions as $i => $expected) {
if ($i !== $expected) {
continue 2; // prefix is not continuous subset
}
}
return true;
}
return false;
}

/**
* Checks if there is any index capable of sorting results in
* required order. This method does not run index check against
* $prefixPrepend
*
* @param array $sort [ field => -1|1, .. ]
* @param array $prefixPrepend list of fields from find() that made an equality check
* @param boolean $allowLessEfficient
* @return boolean
*/
public function areFieldsIndexedForSorting($sort, $prefixPrepend = array(), $allowLessEfficient = true)
{
$indexes = $this->getIndexInfo();
$numFields = count($sort);
foreach ($indexes as $index) {
// no keys or less keys are indexed than we need
if (!isset($index['key']) || count($index['key']) < $numFields) {
continue;
}
// array of index_field => position
$indexFieldPositions = array(); $i = 0;
foreach ($index['key'] as $field => $order) {
$indexFieldPositions[$field] = $i++;
}
$matchedPositions = array();
foreach ($prefixPrepend as $p) {
$matchedPositions[] = $indexFieldPositions[$p];
}
$indexToUse = array(); $currentIndexPosition=-1;
foreach ($sort as $field => $order) {
if (!isset($indexFieldPositions[$field])) {
continue 2; // field is not indexed
}
if ($indexFieldPositions[$field] < $currentIndexPosition) {
continue 2; // wrong field order in index
}
$currentIndexPosition = $indexFieldPositions[$field];
$matchedPositions[] = $currentIndexPosition;
$indexToUse[$field] = $index['key'][$field];
}
sort($matchedPositions);
if ($matchedPositions[0] !== 0) {
continue; // this is not prefix
}
if (!$allowLessEfficient) {
foreach ($matchedPositions as $i => $expected) {
if ($i !== $expected) {
continue 2; // prefix is not continuous subset
}
}
}
// Mongo can traverse index from end to beginning as well
$reversedIndexToUse = array_map(function($order) { return -$order; }, $indexToUse);
if ($sort === $indexToUse || $sort === $reversedIndexToUse) {
return true;
}
}
return false;
}

/**
* Invokes the mapReduce command.
Expand Down
109 changes: 107 additions & 2 deletions tests/Doctrine/MongoDB/Tests/CollectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ public function testInsert()

/**
* @covers Doctrine\MongoDB\Collection::getIndexInfo
* @dataProvider provideIsFieldIndex
* @dataProvider provideIsFieldIndexed
*/
public function testIsFieldIndexed($indexInfo, $field, $expectedResult)
{
Expand All @@ -537,7 +537,7 @@ public function testIsFieldIndexed($indexInfo, $field, $expectedResult)
$this->assertEquals($expectedResult, $coll->isFieldIndexed($field));
}

public function provideIsFieldIndex()
public function provideIsFieldIndexed()
{
$indexInfo = array(
array(
Expand All @@ -559,6 +559,111 @@ public function provideIsFieldIndex()
array($indexInfo, 'baz', false),
);
}

/**
* @covers Doctrine\MongoDB\Collection::getIndexInfo
* @covers Doctrine\MongoDB\Collection::areFieldsIndexed
* @dataProvider provideAreFieldsIndexed
*/
public function testAreFieldsIndexed($indexInfo, $fields, $allowLessEfficient, $expectedResult)
{
$mongoCollection = $this->getMockMongoCollection();

$mongoCollection->expects($this->once())
->method('getIndexInfo')
->will($this->returnValue($indexInfo));

$coll = $this->getTestCollection($this->getMockDatabase(), $mongoCollection);

$this->assertEquals($expectedResult, $coll->areFieldsIndexed($fields, $allowLessEfficient));
}

public function provideAreFieldsIndexed()
{
$indexInfo = array(
array(
'name' => '_id_',
'ns' => 'test.foo',
'key' => array('_id' => 1)
),
array(
'name' => 'foo_1_bar_1_baz_1',
'ns' => 'test.foo',
'key' => array('foo' => 1, 'bar' => 1, 'baz' => 1)
)
);

return array(
array($indexInfo, array('_id'), false, true),
array($indexInfo, array('foo'), false, true),
array($indexInfo, array('bar'), false, false),
array($indexInfo, array('ohmy'), false, false),
array($indexInfo, array('foo', 'baz'), false, false),
array($indexInfo, array('foo', 'baz'), true, true),
array($indexInfo, array('foo', 'ohmy'), false, false),
array($indexInfo, array('baz', 'bar'), false, false),
array($indexInfo, array('foo', 'bar'), false, true),
array($indexInfo, array('baz', 'foo', 'bar'), false, true),
);
}

/**
* @covers Doctrine\MongoDB\Collection::getIndexInfo
* @covers Doctrine\MongoDB\Collection::areFieldsIndexedForSorting
* @dataProvider provideAreFieldsIndexedForSorting
*/
public function testAreFieldsIndexedForSorting($indexInfo, $fields, $prependPrefix, $allowLessEfficient, $expectedResult)
{
$mongoCollection = $this->getMockMongoCollection();

$mongoCollection->expects($this->once())
->method('getIndexInfo')
->will($this->returnValue($indexInfo));

$coll = $this->getTestCollection($this->getMockDatabase(), $mongoCollection);

$this->assertEquals($expectedResult, $coll->areFieldsIndexedForSorting($fields, $prependPrefix, $allowLessEfficient));
}

public function provideAreFieldsIndexedForSorting()
{
$indexInfo = array(
array(
'name' => '_id_',
'ns' => 'test.foo',
'key' => array('_id' => 1)
),
array(
'name' => 'foo_1_bar_1_baz_1',
'ns' => 'test.foo',
'key' => array('foo' => 1, 'bar' => 1, 'baz' => 1)
)
);

return array(
array($indexInfo, array('_id' => 1), array(), false, true),
array($indexInfo, array('_id' => -1), array(), false, true),
array($indexInfo, array('foo' => 1), array(), false, true),
array($indexInfo, array('foo' => -1), array(), false, true),
array($indexInfo, array('bar' => 1), array(), false, false),
array($indexInfo, array('ohmy' => -1), array(), false, false), // not indexed
array($indexInfo, array('foo' => 1, 'baz' => 1), array(), false, false), // inefficient
array($indexInfo, array('foo' => 1, 'baz' => 1), array(), true, true),
array($indexInfo, array('foo' => -1, 'baz' => -1), array(), true, true),
array($indexInfo, array('foo' => 1, 'baz' => -1), array(), true, false), // no index with this sort order
array($indexInfo, array('foo' => 1, 'ohmy' => -1), array(), false, false), // not indexed
array($indexInfo, array('baz' => 1, 'bar' => 1), array(), false, false),
array($indexInfo, array('bar' => 1, 'baz' => 1), array(), false, false), // not a prefix
array($indexInfo, array('bar' => 1, 'baz' => 1), array('foo'), false, true), // can be executed because equality check on foo
array($indexInfo, array('foo' => 1, 'bar' => 1), array(), false, true),
array($indexInfo, array('foo' => -1, 'bar' => -1), array(), false, true),
array($indexInfo, array('foo' => 1, 'bar' => -1), array(), false, false), // no index with this sort order
array($indexInfo, array('foo' => 1, 'bar' => 1, 'baz' => 1), array(), false, true),
array($indexInfo, array('foo' => -1, 'bar' => -1, 'baz' => -1), array(), false, true),
array($indexInfo, array('foo' => 1, 'bar' => 1, 'baz' => -1), array(), false, false), // no index with this sort order
array($indexInfo, array('baz' => 1, 'foo' => 1, 'bar' => 1), array(), false, false), // wrong order of fields
);
}

public function testMapReduceWithResultsInline()
{
Expand Down