Skip to content

Commit 8a4a766

Browse files
committed
Added ignore_conflicts support for #89
1 parent 768f31d commit 8a4a766

File tree

4 files changed

+47
-2
lines changed

4 files changed

+47
-2
lines changed

docs/index.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,9 @@ Keyword Argument Description
220220
``'utf-8'``, and ``'cp437'`` are all valid encoding
221221
parameters.
222222

223+
``ignore_conflicts`` Specify True to ignore unique constraint or exclusion
224+
constraint violation errors. The default is False.
225+
223226
``using`` Sets the database to use when importing data.
224227
Default is None, which will use the ``'default'``
225228
database.

postgres_copy/copy_from.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import csv
99
import logging
1010
from collections import OrderedDict
11+
from django.db import NotSupportedError
1112
from django.db import connections, router
1213
from django.core.exceptions import FieldDoesNotExist
1314
from django.contrib.humanize.templatetags.humanize import intcomma
@@ -30,6 +31,7 @@ def __init__(
3031
force_not_null=None,
3132
force_null=None,
3233
encoding=None,
34+
ignore_conflicts=False,
3335
static_mapping=None
3436
):
3537
# Set the required arguments
@@ -53,6 +55,8 @@ def __init__(
5355
self.force_not_null = force_not_null
5456
self.force_null = force_null
5557
self.encoding = encoding
58+
self.supports_ignore_conflicts = True
59+
self.ignore_conflicts = ignore_conflicts
5660
if static_mapping is not None:
5761
self.static_mapping = OrderedDict(static_mapping)
5862
else:
@@ -70,6 +74,11 @@ def __init__(
7074
if self.conn.vendor != 'postgresql':
7175
raise TypeError("Only PostgreSQL backends supported")
7276

77+
# Check if it is PSQL 9.5 or greater, which determines if ignore_conflicts is supported
78+
self.supports_ignore_conflicts = self.is_postgresql_9_5()
79+
if self.ignore_conflicts and not self.supports_ignore_conflicts:
80+
raise NotSupportedError('This database backend does not support ignoring conflicts.')
81+
7382
# Pull the CSV headers
7483
self.headers = self.get_headers()
7584

@@ -116,6 +125,9 @@ def save(self, silent=False, stream=sys.stdout):
116125

117126
return insert_count
118127

128+
def is_postgresql_9_5(self):
129+
return self.conn.pg_version >= 90500
130+
119131
def get_field(self, name):
120132
"""
121133
Returns any fields on the database model matching the provided name.
@@ -280,6 +292,17 @@ def post_copy(self, cursor):
280292
# INSERT commands
281293
#
282294

295+
def insert_suffix(self):
296+
"""
297+
Preps the suffix to the insert query.
298+
"""
299+
if self.ignore_conflicts:
300+
return """
301+
ON CONFLICT DO NOTHING;
302+
"""
303+
else:
304+
return ";"
305+
283306
def prep_insert(self):
284307
"""
285308
Creates a INSERT statement that reorders and cleans up
@@ -291,11 +314,12 @@ def prep_insert(self):
291314
sql = """
292315
INSERT INTO "%(model_table)s" (%(model_fields)s) (
293316
SELECT %(temp_fields)s
294-
FROM "%(temp_table)s");
317+
FROM "%(temp_table)s")%(insert_suffix)s
295318
"""
296319
options = dict(
297320
model_table=self.model._meta.db_table,
298321
temp_table=self.temp_table_name,
322+
insert_suffix=self.insert_suffix()
299323
)
300324

301325
#

tests/models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,8 @@ def post_insert(self, cursor):
112112
class SecondaryMockObject(models.Model):
113113
text = models.CharField(max_length=500)
114114
objects = CopyManager()
115+
116+
117+
class UniqueMockObject(models.Model):
118+
name = models.CharField(max_length=500, unique=True)
119+
objects = CopyManager()

tests/tests.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
LimitedMockObject,
1111
OverloadMockObject,
1212
HookedCopyMapping,
13-
SecondaryMockObject
13+
SecondaryMockObject,
14+
UniqueMockObject
1415
)
1516
from django.test import TestCase
1617
from django.db.models import Count
@@ -510,6 +511,18 @@ def test_encoding_save(self):
510511
date(2012, 1, 1)
511512
)
512513

514+
def test_ignore_conflicts(self):
515+
UniqueMockObject.objects.from_csv(
516+
self.name_path,
517+
dict(name='NAME'),
518+
ignore_conflicts=True
519+
)
520+
UniqueMockObject.objects.from_csv(
521+
self.name_path,
522+
dict(name='NAME'),
523+
ignore_conflicts=True
524+
)
525+
513526
def test_static_values(self):
514527
ExtendedMockObject.objects.from_csv(
515528
self.name_path,

0 commit comments

Comments
 (0)