Skip to content

Commit c8ebb02

Browse files
authored
Merge pull request #9 from mikefreemanwd/skip-deletes-updates-creates
Support for skip_deletes, skip_creates, and skip_updates in bulk_sync
2 parents bec2bab + 3752b2e commit c8ebb02

File tree

2 files changed

+88
-7
lines changed

2 files changed

+88
-7
lines changed

bulk_sync/__init__.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
from collections import OrderedDict
22
import logging
3-
43
from django.db import transaction
54

65
logger = logging.getLogger(__name__)
76

87

9-
def bulk_sync(new_models, key_fields, filters, batch_size=None, fields=None):
8+
def bulk_sync(new_models, key_fields, filters, batch_size=None, fields=None, skip_creates=False, skip_updates=False, skip_deletes=False):
109
""" Combine bulk create, update, and delete. Make the DB match a set of in-memory objects.
1110
1211
`new_models`: Django ORM objects that are the desired state. They may or may not have `id` set.
@@ -50,15 +49,23 @@ def get_key(obj):
5049
new_obj.id = old_obj.id
5150
existing_objs.append(new_obj)
5251

53-
db_class.objects.bulk_create(new_objs, batch_size=batch_size)
54-
db_class.objects.bulk_update(existing_objs, fields=fields, batch_size=batch_size)
52+
if skip_creates is False:
53+
db_class.objects.bulk_create(new_objs, batch_size=batch_size)
54+
55+
if skip_updates is False:
56+
db_class.objects.bulk_update(existing_objs, fields=fields, batch_size=batch_size)
5557

56-
# delete stale ones...
57-
objs.filter(pk__in=[_.pk for _ in list(obj_dict.values())]).delete()
58+
if skip_deletes is False:
59+
# delete stale objects
60+
objs.filter(pk__in=[_.pk for _ in list(obj_dict.values())]).delete()
5861

5962
assert len(existing_objs) == len(new_models) - len(new_objs)
6063

61-
stats = {"created": len(new_objs), "updated": len(new_models) - len(new_objs), "deleted": len(obj_dict)}
64+
stats = {
65+
"created": 0 if skip_creates else len(new_objs),
66+
"updated": 0 if skip_updates else (len(new_models) - len(new_objs)),
67+
"deleted": 0 if skip_deletes else len(obj_dict)
68+
}
6269

6370
logger.debug(
6471
"{}: {} created, {} updated, {} deleted.".format(

tests/tests.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,80 @@ def test_fields_parameter(self):
104104
self.assertEqual(0, ret["stats"]["created"])
105105
self.assertEqual(0, ret["stats"]["deleted"])
106106

107+
def test_skip_deletes(self):
108+
c1 = Company.objects.create(name="My Company LLC")
109+
110+
e1 = Employee.objects.create(name="Scott", age=40, company=c1)
111+
e2 = Employee.objects.create(name="Isaac", age=9, company=c1)
112+
113+
# update Scott - this makes Isaac is the "stale object" that would be deleted if skip_deletes were False
114+
new_objs = [
115+
Employee(name="Scott", age=41, company=c1),
116+
]
117+
118+
# but Isaac should remain when the skip_deletes flag is True
119+
ret = bulk_sync(new_models=new_objs, filters=None, key_fields=("name",), skip_deletes=True)
120+
121+
self.assertEqual(["Scott", "Isaac"], [x.name for x in Employee.objects.all().order_by('id')])
122+
123+
new_e1 = Employee.objects.get(id=e1.id)
124+
self.assertEqual(41, new_e1.age)
125+
126+
self.assertEqual(2, Employee.objects.count())
127+
128+
self.assertEqual(1, ret["stats"]["updated"])
129+
self.assertEqual(0, ret["stats"]["created"])
130+
self.assertEqual(0, ret["stats"]["deleted"])
131+
132+
def test_skip_creates(self):
133+
c1 = Company.objects.create(name="My Company LLC")
134+
135+
e1 = Employee.objects.create(name="Scott", age=40, company=c1)
136+
e2 = Employee.objects.create(name="Isaac", age=9, company=c1)
137+
138+
# create a new employee that will be ignored
139+
new_objs = [
140+
Employee(name="John", age=52, company=c1)
141+
]
142+
143+
ret = bulk_sync(new_models=new_objs, filters=None, key_fields=("name",), skip_creates=True, skip_deletes=True)
144+
145+
self.assertEqual(2, Employee.objects.count())
146+
self.assertEqual(["Scott", "Isaac"], [x.name for x in Employee.objects.all().order_by('id')])
147+
148+
self.assertEqual(0, ret["stats"]["updated"])
149+
self.assertEqual(0, ret["stats"]["created"])
150+
self.assertEqual(0, ret["stats"]["deleted"])
151+
152+
def test_skip_updates(self):
153+
c1 = Company.objects.create(name="My Company LLC")
154+
155+
e1 = Employee.objects.create(name="Scott", age=40, company=c1)
156+
e2 = Employee.objects.create(name="Isaac", age=9, company=c1)
157+
158+
# update employee that will be ignored, create a new one
159+
new_objs = [
160+
Employee(name="Scott", age=100, company=c1),
161+
Employee(name="Alice", age=36, company=c1)
162+
]
163+
164+
ret = bulk_sync(new_models=new_objs, filters=None, key_fields=("name",), skip_updates=True)
165+
166+
# the age should not have been updated
167+
new_e1 = Employee.objects.get(id=e1.id)
168+
self.assertEqual(40, new_e1.age)
169+
170+
# Isaac is "stale" object - was deleted, Alice was created
171+
self.assertEqual(2, Employee.objects.count())
172+
self.assertEqual(["Scott", "Alice"], [x.name for x in Employee.objects.all().order_by('id')])
173+
174+
175+
self.assertEqual(0, ret["stats"]["updated"])
176+
self.assertEqual(1, ret["stats"]["created"])
177+
self.assertEqual(1, ret["stats"]["deleted"])
178+
179+
180+
107181
class BulkCompareTests(TestCase):
108182
""" Test `bulk_compare` method """
109183

0 commit comments

Comments
 (0)