From 68871ebfab125254214efdb04f982b31d56db324 Mon Sep 17 00:00:00 2001 From: codeliner Date: Wed, 13 Nov 2019 17:02:34 +0100 Subject: [PATCH] Assert unique constraints for existing docs --- src/InMemoryDocumentStore.php | 40 +++++++++++++++--- tests/InMemoryDocumentStoreTest.php | 64 +++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 5 deletions(-) diff --git a/src/InMemoryDocumentStore.php b/src/InMemoryDocumentStore.php index 5c27d38..d51b32c 100644 --- a/src/InMemoryDocumentStore.php +++ b/src/InMemoryDocumentStore.php @@ -15,6 +15,7 @@ use EventEngine\DocumentStore\Exception\RuntimeException; use EventEngine\DocumentStore\Exception\UnknownCollection; use EventEngine\DocumentStore\Filter\AndFilter; +use EventEngine\DocumentStore\Filter\AnyFilter; use EventEngine\DocumentStore\Filter\EqFilter; use EventEngine\DocumentStore\Filter\Filter; use EventEngine\DocumentStore\OrderBy\AndOrder; @@ -89,6 +90,7 @@ public function hasCollectionIndex(string $collectionName, string $indexName): b { foreach ($this->inMemoryConnection['documentIndices'][$collectionName] as $index) { if($index instanceof FieldIndex || $index instanceof MultiFieldIndex) { + echo $index->name(); if($index->name() === $indexName) { return true; } @@ -105,11 +107,39 @@ public function hasCollectionIndex(string $collectionName, string $indexName): b */ public function addCollectionIndex(string $collectionName, Index $index): void { + $this->assertHasCollection($collectionName); + if($index instanceof FieldIndex || $index instanceof MultiFieldIndex) { $this->dropCollectionIndex($collectionName, $index->name()); } - $this->inMemoryConnection['documentIndices'][] = $index; + $docsCount = count($this->inMemoryConnection['documents'][$collectionName]); + + if($docsCount > 1 && ($index instanceof FieldIndex || $index instanceof MultiFieldIndex) && $index->unique()) { + $uniqueErrMsg = "Unique constraint violation. Cannot add unique index because existing documents conflict with it!"; + $assertMethod = $index instanceof FieldIndex? 'assertUniqueFieldConstraint' : 'assertMultiFieldUniqueConstraint'; + $checkCount = 0; + $halfOfDocs = $docsCount / 2; + + foreach ($this->inMemoryConnection['documents'][$collectionName] as $docId => $document) { + if($checkCount > $halfOfDocs) { + break; + } + + // Temp unset to prevent false positives + unset($this->inMemoryConnection['documents'][$collectionName][$docId]); + try { + $this->{$assertMethod}($collectionName, (string)$docId, $document, $index, $uniqueErrMsg); + } catch (\Throwable $e) { + $this->inMemoryConnection['documents'][$collectionName][$docId] = $document; + throw $e; + } + + $checkCount++; + } + } + + $this->inMemoryConnection['documentIndices'][$collectionName][] = $index; } /** @@ -326,7 +356,7 @@ private function assertUniqueConstraints(string $collectionName, string $docId, } } - private function assertUniqueFieldConstraint(string $collectionName, string $docId, array $docOrSubset, FieldIndex $index): void + private function assertUniqueFieldConstraint(string $collectionName, string $docId, array $docOrSubset, FieldIndex $index, string $errMsg = null): void { if(!$index->unique()) { return; @@ -346,14 +376,14 @@ private function assertUniqueFieldConstraint(string $collectionName, string $doc foreach ($existingDocs as $existingDoc) { throw new RuntimeException( - "Unique constraint violation. Cannot insert or update document with id $docId, because a document with same value for field: {$index->field()} exists already!" + $errMsg ?? "Unique constraint violation. Cannot insert or update document with id $docId, because a document with same value for field: {$index->field()} exists already!" ); } return; } - private function assertMultiFieldUniqueConstraint(string $collectionName, string $docId, array $docOrSubset, MultiFieldIndex $index): void + private function assertMultiFieldUniqueConstraint(string $collectionName, string $docId, array $docOrSubset, MultiFieldIndex $index, string $errMsg = null): void { if(!$index->unique()) { return; @@ -402,7 +432,7 @@ private function assertMultiFieldUniqueConstraint(string $collectionName, string foreach ($existingDocs as $existingDoc) { $fieldNamesStr = implode(", ", $fieldNames); throw new RuntimeException( - "Unique constraint violation. Cannot insert or update document with id $docId, because a document with same values for fields: {$fieldNamesStr} exists already!" + $errMsg ?? "Unique constraint violation. Cannot insert or update document with id $docId, because a document with same values for fields: {$fieldNamesStr} exists already!" ); } diff --git a/tests/InMemoryDocumentStoreTest.php b/tests/InMemoryDocumentStoreTest.php index 7fe2c8e..315eb84 100644 --- a/tests/InMemoryDocumentStoreTest.php +++ b/tests/InMemoryDocumentStoreTest.php @@ -136,4 +136,68 @@ public function it_ensures_unique_constraints_for_multiple_fields() $this->expectExceptionMessageRegExp('/^Unique constraint violation/'); $this->store->updateDoc('test', '2', ['some' => ['prop' => 'foo']]); } + + /** + * @test + */ + public function it_blocks_adding_a_unique_index_if_it_conflicts_with_existing_docs() + { + $this->store->addCollection('test'); + + $this->store->addDoc('test', '1', ['some' => ['prop' => 'foo', 'other' => ['prop' => 'bat']]]); + $this->store->addDoc('test', '2', ['some' => ['prop' => 'bar', 'other' => ['prop' => 'bat']]]); + $this->store->addDoc('test', '3', ['some' => ['prop' => 'bar']]); + + $this->expectExceptionMessageRegExp('/^Unique constraint violation/'); + $uniqueIndex = FieldIndex::forField('some.prop', FieldIndex::SORT_ASC, true); + $this->store->addCollectionIndex('test', $uniqueIndex); + } + + /** + * @test + */ + public function it_does_not_block_adding_a_unique_index_if_no_conflict_exists() + { + $this->store->addCollection('test'); + + $this->store->addDoc('test', '1', ['some' => ['prop' => 'foo', 'other' => ['prop' => 'bat']]]); + $this->store->addDoc('test', '2', ['some' => ['prop' => 'bar', 'other' => ['prop' => 'baz']]]); + $this->store->addDoc('test', '3', ['some' => ['prop' => 'bar']]); + + $uniqueIndex = FieldIndex::namedIndexForField('test_idx', 'some.other.prop', FieldIndex::SORT_ASC, true); + $this->store->addCollectionIndex('test', $uniqueIndex); + $this->assertTrue($this->store->hasCollectionIndex('test', 'test_idx')); + } + + /** + * @test + */ + public function it_blocks_adding_a_unique_multi_field_index_if_it_conflicts_with_existing_docs() + { + $this->store->addCollection('test'); + + $this->store->addDoc('test', '1', ['some' => ['prop' => 'foo', 'other' => ['prop' => 'bat']]]); + $this->store->addDoc('test', '2', ['some' => ['prop' => 'foo', 'other' => ['prop' => 'bat']]]); + $this->store->addDoc('test', '3', ['some' => ['prop' => 'bar']]); + + $this->expectExceptionMessageRegExp('/^Unique constraint violation/'); + $uniqueIndex = MultiFieldIndex::forFields(['some.prop', 'some.other.prop'], true); + $this->store->addCollectionIndex('test', $uniqueIndex); + } + + /** + * @test + */ + public function it_does_not_block_adding_a_unique_multi_field_index_if_no_conflict_exists() + { + $this->store->addCollection('test'); + + $this->store->addDoc('test', '1', ['some' => ['prop' => 'foo', 'other' => ['prop' => 'bat']]]); + $this->store->addDoc('test', '2', ['some' => ['prop' => 'bar', 'other' => ['prop' => 'bat']]]); + $this->store->addDoc('test', '3', ['some' => ['prop' => 'bar']]); + + $uniqueIndex = MultiFieldIndex::namedIndexForFields('test_idx', ['some.prop', 'some.other.prop'], true); + $this->store->addCollectionIndex('test', $uniqueIndex); + $this->assertTrue($this->store->hasCollectionIndex('test', 'test_idx')); + } }