Skip to content

Commit

Permalink
Merge pull request #1829 from jmikola/gh1821
Browse files Browse the repository at this point in the history
Improve SchemaManager logic for comparing text indexes
  • Loading branch information
jmikola committed Jul 18, 2018
2 parents 8de00c9 + 0f238b1 commit 183805f
Show file tree
Hide file tree
Showing 2 changed files with 234 additions and 3 deletions.
87 changes: 84 additions & 3 deletions lib/Doctrine/ODM/MongoDB/SchemaManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use function array_filter;
use function array_unique;
use function iterator_to_array;
use function ksort;
use function strpos;

class SchemaManager
Expand Down Expand Up @@ -391,7 +392,7 @@ public function isMongoIndexEquivalentToDocumentIndex($mongoIndex, $documentInde
{
$documentIndexOptions = $documentIndex['options'];

if ($mongoIndex['key'] !== $documentIndex['keys']) {
if (! $this->isEquivalentIndexKeys($mongoIndex, $documentIndex)) {
return false;
}

Expand All @@ -413,7 +414,7 @@ public function isMongoIndexEquivalentToDocumentIndex($mongoIndex, $documentInde
return false;
}

if (isset($mongoIndex[$option]) && isset($documentIndexOptions[$option]) &&
if (isset($mongoIndex[$option], $documentIndexOptions[$option]) &&
$mongoIndex[$option] !== $documentIndexOptions[$option]) {
return false;
}
Expand All @@ -423,14 +424,94 @@ public function isMongoIndexEquivalentToDocumentIndex($mongoIndex, $documentInde
return false;
}

if (isset($mongoIndex['partialFilterExpression']) && isset($documentIndexOptions['partialFilterExpression']) &&
if (isset($mongoIndex['partialFilterExpression'], $documentIndexOptions['partialFilterExpression']) &&
$mongoIndex['partialFilterExpression'] !== $documentIndexOptions['partialFilterExpression']) {
return false;
}

if (isset($mongoIndex['weights']) && ! $this->isEquivalentTextIndexWeights($mongoIndex, $documentIndex)) {
return false;
}

foreach (['default_language', 'language_override', 'textIndexVersion'] as $option) {
/* Text indexes will always report defaults for these options, so
* only compare if we have explicit values in the document index. */
if (isset($mongoIndex[$option], $documentIndexOptions[$option]) &&
$mongoIndex[$option] !== $documentIndexOptions[$option]) {
return false;
}
}

return true;
}

/**
* Determine if the keys for a MongoDB index can be considered equivalent to
* those for an index in class metadata.
*
* @param array|IndexInfo $mongoIndex Mongo index data.
* @param array $documentIndex Document index data.
* @return bool True if the indexes have equivalent keys, otherwise false.
*/
private function isEquivalentIndexKeys($mongoIndex, array $documentIndex)
{
$mongoIndexKeys = $mongoIndex['key'];
$documentIndexKeys = $documentIndex['keys'];

/* If we are dealing with text indexes, we need to unset internal fields
* from the MongoDB index and filter out text fields from the document
* index. This will leave only non-text fields, which we can compare as
* normal. Any text fields in the document index will be compared later
* with isEquivalentTextIndexWeights(). */
if (isset($mongoIndexKeys['_fts']) && $mongoIndexKeys['_fts'] === 'text') {
unset($mongoIndexKeys['_fts'], $mongoIndexKeys['_ftsx']);

$documentIndexKeys = array_filter($documentIndexKeys, function ($type) {
return $type !== 'text';
});
}

/* Avoid a strict equality check here. The numeric type returned by
* MongoDB may differ from the document index without implying that the
* indexes themselves are inequivalent. */
// phpcs:disable SlevomatCodingStandard.ControlStructures.DisallowEqualOperators.DisallowedEqualOperator
return $mongoIndexKeys == $documentIndexKeys;
}

/**
* Determine if the text index weights for a MongoDB index can be considered
* equivalent to those for an index in class metadata.
*
* @param array|IndexInfo $mongoIndex Mongo index data.
* @param array $documentIndex Document index data.
* @return bool True if the indexes have equivalent weights, otherwise false.
*/
private function isEquivalentTextIndexWeights($mongoIndex, array $documentIndex)
{
$mongoIndexWeights = $mongoIndex['weights'];
$documentIndexWeights = $documentIndex['options']['weights'] ?? [];

// If not specified, assign a default weight for text fields
foreach ($documentIndex['keys'] as $key => $type) {
if ($type !== 'text' || isset($documentIndexWeights[$key])) {
continue;
}

$documentIndexWeights[$key] = 1;
}

/* MongoDB returns the weights sorted by field name, but we'll sort both
* arrays in case that is internal behavior not to be relied upon. */
ksort($mongoIndexWeights);
ksort($documentIndexWeights);

/* Avoid a strict equality check here. The numeric type returned by
* MongoDB may differ from the document index without implying that the
* indexes themselves are inequivalent. */
// phpcs:disable SlevomatCodingStandard.ControlStructures.DisallowEqualOperators.DisallowedEqualOperator
return $mongoIndexWeights == $documentIndexWeights;
}

/**
* Ensure collections are sharded for all documents that can be loaded with the
* metadata factory.
Expand Down
150 changes: 150 additions & 0 deletions tests/Doctrine/ODM/MongoDB/Tests/SchemaManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,11 @@ public function dataIsMongoIndexEquivalentToDocumentIndex()
'mongoIndex' => ['key' => ['foo' => 1]],
'documentIndex' => ['keys' => ['foo' => 1]],
],
'keysSameButNumericTypesDiffer' => [
'expected' => true,
'mongoIndex' => ['key' => ['foo' => 1.0]],
'documentIndex' => ['keys' => ['foo' => 1]],
],
'keysDiffer' => [
'expected' => false,
'mongoIndex' => ['key' => ['foo' => 1]],
Expand Down Expand Up @@ -462,6 +467,151 @@ public function dataIsMongoIndexEquivalentToDocumentIndex()
'mongoIndex' => ['partialFilterExpression' => []],
'documentIndex' => [],
],
// Comparing non-text Mongo index with text document index
'textIndex' => [
'expected' => false,
'mongoIndex' => [],
'documentIndex' => ['keys' => ['foo' => 'text', 'bar' => 'text']],
],
];
}

/**
* @dataProvider dataIsMongoTextIndexEquivalentToDocumentIndex
*/
public function testIsMongoIndexEquivalentToDocumentIndexWithTextIndexes($expected, $mongoIndex, $documentIndex)
{
$defaultMongoIndex = [
'key' => ['_fts' => 'text', '_ftsx' => 1],
'weights' => ['bar' => 1, 'foo' => 1],
'default_language' => 'english',
'language_override' => 'language',
'textIndexVersion' => 3,
];
$defaultDocumentIndex = [
'keys' => ['foo' => 'text', 'bar' => 'text'],
'options' => [],
];

$mongoIndex += $defaultMongoIndex;
$documentIndex += $defaultDocumentIndex;

$this->assertSame($expected, $this->schemaManager->isMongoIndexEquivalentToDocumentIndex($mongoIndex, $documentIndex));
}

public function dataIsMongoTextIndexEquivalentToDocumentIndex()
{
return [
'keysSame' => [
'expected' => true,
'mongoIndex' => [],
'documentIndex' => ['keys' => ['foo' => 'text', 'bar' => 'text']],
],
'keysSameWithDifferentOrder' => [
'expected' => true,
'mongoIndex' => [],
'documentIndex' => ['keys' => ['bar' => 'text', 'foo' => 'text']],
],
'keysDiffer' => [
'expected' => false,
'mongoIndex' => [],
'documentIndex' => ['keys' => ['x' => 'text', 'y' => 'text']],
],
'compoundIndexKeysSameAndWeightsSame' => [
'expected' => true,
'mongoIndex' => [
'key' => ['_fts' => 'text', '_ftsx' => 1, 'a' => -1, 'd' => 1],
'weights' => ['b' => 1, 'c' => 2],
],
'documentIndex' => [
'keys' => ['a' => -1, 'b' => 'text', 'c' => 'text', 'd' => 1],
'options' => ['weights' => ['b' => 1, 'c' => 2]],
],
],
'compoundIndexKeysSameAndWeightsDiffer' => [
'expected' => false,
'mongoIndex' => [
'key' => ['_fts' => 'text', '_ftsx' => 1, 'a' => -1, 'd' => 1],
'weights' => ['b' => 1, 'c' => 2],
],
'documentIndex' => [
'keys' => ['a' => -1, 'b' => 'text', 'c' => 'text', 'd' => 1],
'options' => ['weights' => ['b' => 3, 'c' => 2]],
],
],
'compoundIndexKeysDifferAndWeightsSame' => [
'expected' => false,
'mongoIndex' => [
'key' => ['_fts' => 'text', '_ftsx' => 1, 'a' => 1, 'd' => 1],
'weights' => ['b' => 1, 'c' => 2],
],
'documentIndex' => [
'keys' => ['a' => -1, 'b' => 'text', 'c' => 'text', 'd' => 1],
'options' => ['weights' => ['b' => 1, 'c' => 2]],
],
],
'weightsSame' => [
'expected' => true,
'mongoIndex' => [
'weights' => ['a' => 1, 'b' => 2],
],
'documentIndex' => [
'keys' => ['a' => 'text', 'b' => 'text'],
'options' => ['weights' => ['a' => 1, 'b' => 2]],
],
],
'weightsSameAfterSorting' => [
'expected' => true,
'mongoIndex' => [
/* MongoDB returns the weights sorted by field name, but
* simulate an unsorted response to test our own ksort(). */
'weights' => ['a' => 1, 'c' => 3, 'b' => 2],
],
'documentIndex' => [
'keys' => ['a' => 'text', 'b' => 'text', 'c' => 'text'],
'options' => ['weights' => ['c' => 3, 'a' => 1, 'b' => 2]],
],
],
'weightsSameButNumericTypesDiffer' => [
'expected' => true,
'mongoIndex' => [
'weights' => ['a' => 1, 'b' => 2],
],
'documentIndex' => [
'keys' => ['a' => 'text', 'b' => 'text'],
'options' => ['weights' => ['a' => 1.0, 'b' => 2.0]],
],
],
'defaultLanguageSame' => [
'expected' => true,
'mongoIndex' => [],
'documentIndex' => ['options' => ['default_language' => 'english']],
],
'defaultLanguageMismatch' => [
'expected' => false,
'mongoIndex' => [],
'documentIndex' => ['options' => ['default_language' => 'german']],
],
'languageOverrideSame' => [
'expected' => true,
'mongoIndex' => [],
'documentIndex' => ['options' => ['language_override' => 'language']],
],
'languageOverrideMismatch' => [
'expected' => false,
'mongoIndex' => [],
'documentIndex' => ['options' => ['language_override' => 'idioma']],
],
'textIndexVersionSame' => [
'expected' => true,
'mongoIndex' => [],
'documentIndex' => ['options' => ['textIndexVersion' => 3]],
],
'textIndexVersionMismatch' => [
'expected' => false,
'mongoIndex' => [],
'documentIndex' => ['options' => ['textIndexVersion' => 2]],
],
];
}

Expand Down

0 comments on commit 183805f

Please sign in to comment.