Repository: proteanhq/protean

Issue:
Add support for `update_by` and `delete_by`

Discussion:

Patch:
diff --git a/src/protean/core/entity.py b/src/protean/core/entity.py
index a1a28aa0..2d5bfbed 100644
--- a/src/protean/core/entity.py
+++ b/src/protean/core/entity.py
@@ -61,11 +61,19 @@ class EntityBase(type):
         # Load list of Attributes from declared fields, depending on type of fields
         new_class._load_attributes()
 
-        # Construct an empty QuerySet associated with this Entity class
-        new_class.query = QuerySet(name)
-
         return new_class
 
+    @property
+    def query(cls):
+        """Construct an empty QuerySet associated with an Entity class
+            everytime a new `query` object is created
+
+        This is required so as not to corrupt the query object associated with the metaclass
+        when invoked like `Dog.query.all()` directly. A new query, and a corresponding `Pagination`
+        result would be created every time.
+        """
+        return QuerySet(cls.__name__)
+
     def _load_base_class_fields(new_class, bases, attrs):
         """If this class is subclassing another Entity, add that Entity's
         fields.  Note that we loop over the bases in *reverse*.
@@ -305,6 +313,48 @@ class QuerySet:
 
         return results
 
+    def update(self, *data, **kwargs):
+        """Updates all objects with details given if they match a set of conditions supplied.
+
+        Returns the number of objects matched (which may not be equal to the number of objects
+            updated if objects rows already have the new value).
+        """
+        updated_item_count = 0
+        try:
+            items = self.all()
+
+            for item in items:
+                item.update(*data, **kwargs)
+                updated_item_count += 1
+        except Exception as exc:
+            # FIXME Log Exception
+            raise
+
+        return updated_item_count
+
+    def delete(self):
+        """Deletes matching objects from the Repository
+
+        Does not throw error if no objects are matched.
+        Throws ObjectNotFoundError if the object was not found in the repository
+
+        Returns the number of objects matched (which may not be equal to the number of objects
+            deleted if objects rows already have the new value).
+        """
+        # Fetch Model class and connected-adapter from Repository Factory
+        deleted_item_count = 0
+        try:
+            items = self.all()
+
+            for item in items:
+                item.delete()
+                deleted_item_count += 1
+        except Exception as exc:
+            # FIXME Log Exception
+            raise
+
+        return deleted_item_count
+
     ###############################
     # Python Magic method support #
     ###############################


Test patch:
diff --git a/tests/core/test_entity.py b/tests/core/test_entity.py
index 2c1bf20a..de314e2f 100644
--- a/tests/core/test_entity.py
+++ b/tests/core/test_entity.py
@@ -271,6 +271,28 @@ class TestEntity:
         with pytest.raises(ValidationError):
             dog.update(age='x')
 
+    def test_update_by(self):
+        """Test that update by query updates only correct records"""
+        Dog.create(id=1, name='Athos', owner='John', age=2)
+        Dog.create(id=2, name='Porthos', owner='John', age=3)
+        Dog.create(id=3, name='Aramis', owner='John', age=4)
+        Dog.create(id=4, name='dArtagnan', owner='John', age=5)
+
+        # Perform update
+        updated_count = Dog.query.filter(age__gt=3).update(owner='Jane')
+
+        # Query and check if only the relevant records have been updated
+        assert updated_count == 2
+
+        u_dog1 = Dog.get(1)
+        u_dog2 = Dog.get(2)
+        u_dog3 = Dog.get(3)
+        u_dog4 = Dog.get(4)
+        assert u_dog1.owner == 'John'
+        assert u_dog2.owner == 'John'
+        assert u_dog3.owner == 'Jane'
+        assert u_dog4.owner == 'Jane'
+
     def test_unique(self):
         """ Test the unique constraints for the entity """
         Dog.create(id=2, name='Johnny', owner='Carey')
@@ -490,6 +512,28 @@ class TestEntity:
         with pytest.raises(ObjectNotFoundError):
             Dog.get(3)
 
+    def test_delete_by(self):
+        """Test that update by query updates only correct records"""
+        Dog.create(id=1, name='Athos', owner='John', age=2)
+        Dog.create(id=2, name='Porthos', owner='John', age=3)
+        Dog.create(id=3, name='Aramis', owner='John', age=4)
+        Dog.create(id=4, name='dArtagnan', owner='John', age=5)
+
+        # Perform update
+        deleted_count = Dog.query.filter(age__gt=3).delete()
+
+        # Query and check if only the relevant records have been updated
+        assert deleted_count == 2
+        assert Dog.query.all().total == 2
+
+        assert Dog.get(1) is not None
+        assert Dog.get(2) is not None
+        with pytest.raises(ObjectNotFoundError):
+            Dog.get(3)
+
+        with pytest.raises(ObjectNotFoundError):
+            Dog.get(4)
+
     def test_filter_returns_q_object(self):
         """Test Negation of a criteria"""
         # Add multiple entries to the DB