Repository: MongoEngine/mongoengine

Issue:
Unique does not work for EmbeddedDocuments in ListFields
It seems that the intended functionality of and EmbeddedDocument would not allow a field marked as unique to be duplicated in another EmbeddedDocument in the same ListField (which, for EmbeddedDocuments, is like a collection).



Discussion:
Hi @stephenmac7,

Thanks for the ticket, it would help to have a test case to describe what you mean, so I can understand it better. 

However, I think there are some limitations in MongoDB with multikey unique indexes. eg:

```
// In the shell
> db.unique.save({item: [{name: "ross"}, {name: "ross"}]})
> db.unique.find()
{ "_id" : ObjectId("51af07ad11d7d648c74c0544"), "item" : [ { "name" : "ross" }, { "name" : "ross" } ] }

// Try and save again
> db.unique.save({"item": [{"name": "ross"}]})
E11000 duplicate key error index: test.unique.$item.name_1  dup key: { : "ross" }
```

That's not what I mean. Sorry. Something like

``` python
class EmbDoc(EmbeddedDocument):
    x = StringField(required=True, unique=True)

class Doc(Document):
    l = ListField(EmbeddedDocumentField(EmbDoc))
```

Now, you can have as many EmbDocs the same x in Doc.l, even though it is 'unique'. It would seem that this behavior is not intended:

``` python
d = Doc(l=[EmbDoc(x='t'), EmbDoc(x='t')]).save()
```

The above should raise an exception, correct?

Unique is handled by an index. Does a unique index exist for the document?

On Sunday, June 23, 2013, stephenmac7 wrote:

> The above should raise an exception, correct?
> 
> —
> Reply to this email directly or view it on GitHubhttps://github.com/MongoEngine/mongoengine/issues/358#issuecomment-19867765
> .

I have the situation described in the example above. There is no index explicitly defined at the moment.

You can manually create an index with:

```
meta = {
    'indexes': [
        {
            'fields': ['l.x'],
            'unique': True
    ]
}
```

However, I think MongoEngine should create this index automatically if `unique=True` on the field in the `Document`. It works for `EmbeddedDocumentField`, but doesn't if it's wrapped with a `ListField`. Where is the logic in that?

I see. The reason I wanted the unique to work no longer exists, however it's nice to have this as a reference for future use :+1: 


Resolving patch:
diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py
--- a/mongoengine/base/document.py
+++ b/mongoengine/base/document.py
@@ -816,6 +816,9 @@ def _unique_with_indexes(cls, namespace=""):
                 index = {'fields': fields, 'unique': True, 'sparse': sparse}
                 unique_indexes.append(index)
 
+            if field.__class__.__name__ == "ListField":
+                field = field.field
+
             # Grab any embedded document field unique indexes
             if (field.__class__.__name__ == "EmbeddedDocumentField" and
                     field.document_type != cls):


Test patch:
diff --git a/tests/document/indexes.py b/tests/document/indexes.py
--- a/tests/document/indexes.py
+++ b/tests/document/indexes.py
@@ -577,6 +577,38 @@ class BlogPost(Document):
 
         BlogPost.drop_collection()
 
+    def test_unique_embedded_document_in_list(self):
+        """
+        Ensure that the uniqueness constraints are applied to fields in
+        embedded documents, even when the embedded documents in in a
+        list field.
+        """
+        class SubDocument(EmbeddedDocument):
+            year = IntField(db_field='yr')
+            slug = StringField(unique=True)
+
+        class BlogPost(Document):
+            title = StringField()
+            subs = ListField(EmbeddedDocumentField(SubDocument))
+
+        BlogPost.drop_collection()
+
+        post1 = BlogPost(
+            title='test1', subs=[
+                SubDocument(year=2009, slug='conflict'),
+                SubDocument(year=2009, slug='conflict')
+            ]
+        )
+        post1.save()
+
+        post2 = BlogPost(
+            title='test2', subs=[SubDocument(year=2014, slug='conflict')]
+        )
+
+        self.assertRaises(NotUniqueError, post2.save)
+
+        BlogPost.drop_collection()
+
     def test_unique_with_embedded_document_and_embedded_unique(self):
         """Ensure that uniqueness constraints are applied to fields on
         embedded documents.  And work with unique_with as well.
diff --git a/tests/fields/fields.py b/tests/fields/fields.py
--- a/tests/fields/fields.py
+++ b/tests/fields/fields.py
@@ -1974,14 +1974,14 @@ class Employee(Document):
     def test_recursive_embedding(self):
         """Ensure that EmbeddedDocumentFields can contain their own documents.
         """
-        class Tree(Document):
-            name = StringField()
-            children = ListField(EmbeddedDocumentField('TreeNode'))
-
         class TreeNode(EmbeddedDocument):
             name = StringField()
             children = ListField(EmbeddedDocumentField('self'))
 
+        class Tree(Document):
+            name = StringField()
+            children = ListField(EmbeddedDocumentField('TreeNode'))
+
         Tree.drop_collection()
         tree = Tree(name="Tree")
 
diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py
--- a/tests/queryset/queryset.py
+++ b/tests/queryset/queryset.py
@@ -512,16 +512,16 @@ class BlogPost(Document):
 
     def test_updates_can_have_match_operators(self):
 
-        class Post(Document):
-            title = StringField(required=True)
-            tags = ListField(StringField())
-            comments = ListField(EmbeddedDocumentField("Comment"))
-
         class Comment(EmbeddedDocument):
             content = StringField()
             name = StringField(max_length=120)
             vote = IntField()
 
+        class Post(Document):
+            title = StringField(required=True)
+            tags = ListField(StringField())
+            comments = ListField(EmbeddedDocumentField("Comment"))
+
         Post.drop_collection()
 
         comm1 = Comment(content="very funny indeed", name="John S", vote=1)
diff --git a/tests/test_dereference.py b/tests/test_dereference.py
--- a/tests/test_dereference.py
+++ b/tests/test_dereference.py
@@ -318,6 +318,10 @@ class SimpleList(Document):
     def test_circular_reference(self):
         """Ensure you can handle circular references
         """
+        class Relation(EmbeddedDocument):
+            name = StringField()
+            person = ReferenceField('Person')
+
         class Person(Document):
             name = StringField()
             relations = ListField(EmbeddedDocumentField('Relation'))
@@ -325,10 +329,6 @@ class Person(Document):
             def __repr__(self):
                 return "<Person: %s>" % self.name
 
-        class Relation(EmbeddedDocument):
-            name = StringField()
-            person = ReferenceField('Person')
-
         Person.drop_collection()
         mother = Person(name="Mother")
         daughter = Person(name="Daughter")
@@ -1220,14 +1220,15 @@ class Page(Document):
         self.assertEqual(page.tags[0], page.posts[0].tags[0])
 
     def test_select_related_follows_embedded_referencefields(self):
-        class Playlist(Document):
-            items = ListField(EmbeddedDocumentField("PlaylistItem"))
+
+        class Song(Document):
+            title = StringField()
 
         class PlaylistItem(EmbeddedDocument):
             song = ReferenceField("Song")
 
-        class Song(Document):
-            title = StringField()
+        class Playlist(Document):
+            items = ListField(EmbeddedDocumentField("PlaylistItem"))
 
         Playlist.drop_collection()
         Song.drop_collection()