Skip to content

Commit 0124f08

Browse files
authored
Merge pull request #46 from PeterJCLaw/fix-foreign-key-on-conflict
Cope with foreign key values being specified as objects
2 parents 6eb2f84 + 34142d1 commit 0124f08

File tree

3 files changed

+226
-1
lines changed

3 files changed

+226
-1
lines changed

psqlextra/compiler.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,12 @@ def _format_field_value(self, field_name) -> str:
271271
return SQLInsertCompiler.prepare_value(
272272
self,
273273
field,
274-
getattr(self.query.objs[0], field_name)
274+
# Note: this deliberately doesn't use `pre_save_val` as we don't
275+
# want things like auto_now on DateTimeField (etc.) to change the
276+
# value. We rely on pre_save having already been done by the
277+
# underlying compiler so that things like FileField have already had
278+
# the opportunity to save out their data.
279+
getattr(self.query.objs[0], field.attname)
275280
)
276281

277282
def _normalize_field_name(self, field_name) -> str:

tests/test_on_conflict_nothing.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import pytest
12
from django.db import models
23

34
from psqlextra.fields import HStoreField
@@ -39,3 +40,112 @@ def test_on_conflict_nothing():
3940
assert obj1.cookies == 'cheers'
4041
assert obj2.title['key1'] == 'beer'
4142
assert obj2.cookies == 'cheers'
43+
44+
45+
def test_on_conflict_nothing_foreign_key_by_object():
46+
"""
47+
Tests whether simple insert NOTHING works correctly when the potentially
48+
conflicting field is a foreign key specified as an object.
49+
"""
50+
51+
other_model = get_fake_model({})
52+
53+
model = get_fake_model({
54+
'other': models.OneToOneField(
55+
other_model,
56+
on_delete=models.CASCADE,
57+
),
58+
'data': models.CharField(max_length=255),
59+
})
60+
61+
other_obj = other_model.objects.create()
62+
63+
obj1 = (
64+
model.objects
65+
.on_conflict(['other'], ConflictAction.NOTHING)
66+
.insert_and_get(other=other_obj, data="some data")
67+
)
68+
69+
assert obj1.other == other_obj
70+
assert obj1.data == "some data"
71+
72+
obj1.refresh_from_db()
73+
assert obj1.other == other_obj
74+
assert obj1.data == "some data"
75+
76+
with pytest.raises(ValueError):
77+
(
78+
model.objects
79+
.on_conflict(['other'], ConflictAction.NOTHING)
80+
.insert_and_get(other=obj1)
81+
)
82+
83+
obj2 = (
84+
model.objects
85+
.on_conflict(['other'], ConflictAction.NOTHING)
86+
.insert_and_get(other=other_obj, data="different data")
87+
)
88+
89+
assert obj2.other == other_obj
90+
assert obj2.data == "some data"
91+
92+
obj1.refresh_from_db()
93+
obj2.refresh_from_db()
94+
95+
# assert that the 'other' field didn't change
96+
assert obj1.id == obj2.id
97+
assert obj1.other == other_obj
98+
assert obj2.other == other_obj
99+
assert obj1.data == "some data"
100+
assert obj2.data == "some data"
101+
102+
103+
def test_on_conflict_nothing_foreign_key_by_id():
104+
"""
105+
Tests whether simple insert NOTHING works correctly when the potentially
106+
conflicting field is a foreign key specified as an id.
107+
"""
108+
109+
other_model = get_fake_model({})
110+
111+
model = get_fake_model({
112+
'other': models.OneToOneField(
113+
other_model,
114+
on_delete=models.CASCADE,
115+
),
116+
'data': models.CharField(max_length=255),
117+
})
118+
119+
other_obj = other_model.objects.create()
120+
121+
obj1 = (
122+
model.objects
123+
.on_conflict(['other_id'], ConflictAction.NOTHING)
124+
.insert_and_get(other_id=other_obj.pk, data="some data")
125+
)
126+
127+
assert obj1.other == other_obj
128+
assert obj1.data == "some data"
129+
130+
obj1.refresh_from_db()
131+
assert obj1.other == other_obj
132+
assert obj1.data == "some data"
133+
134+
obj2 = (
135+
model.objects
136+
.on_conflict(['other_id'], ConflictAction.NOTHING)
137+
.insert_and_get(other_id=other_obj.pk, data="different data")
138+
)
139+
140+
assert obj2.other == other_obj
141+
assert obj2.data == "some data"
142+
143+
obj1.refresh_from_db()
144+
obj2.refresh_from_db()
145+
146+
# assert that the 'other' field didn't change
147+
assert obj1.id == obj2.id
148+
assert obj1.other == other_obj
149+
assert obj2.other == other_obj
150+
assert obj1.data == "some data"
151+
assert obj2.data == "some data"

tests/test_on_conflict_update.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import pytest
12
from django.db import models
23

34
from psqlextra.fields import HStoreField
@@ -39,3 +40,112 @@ def test_on_conflict_update():
3940
assert obj1.cookies == 'choco'
4041
assert obj2.title['key1'] == 'beer'
4142
assert obj2.cookies == 'choco'
43+
44+
45+
def test_on_conflict_update_foreign_key_by_object():
46+
"""
47+
Tests whether simple upsert works correctly when the conflicting field is a
48+
foreign key specified as an object.
49+
"""
50+
51+
other_model = get_fake_model({})
52+
53+
model = get_fake_model({
54+
'other': models.OneToOneField(
55+
other_model,
56+
on_delete=models.CASCADE,
57+
),
58+
'data': models.CharField(max_length=255),
59+
})
60+
61+
other_obj = other_model.objects.create()
62+
63+
obj1 = (
64+
model.objects
65+
.on_conflict(['other'], ConflictAction.UPDATE)
66+
.insert_and_get(other=other_obj, data="some data")
67+
)
68+
69+
assert obj1.other == other_obj
70+
assert obj1.data == "some data"
71+
72+
obj1.refresh_from_db()
73+
assert obj1.other == other_obj
74+
assert obj1.data == "some data"
75+
76+
with pytest.raises(ValueError):
77+
(
78+
model.objects
79+
.on_conflict(['other'], ConflictAction.UPDATE)
80+
.insert_and_get(other=obj1)
81+
)
82+
83+
obj2 = (
84+
model.objects
85+
.on_conflict(['other'], ConflictAction.UPDATE)
86+
.insert_and_get(other=other_obj, data="different data")
87+
)
88+
89+
assert obj2.other == other_obj
90+
assert obj2.data == "different data"
91+
92+
obj1.refresh_from_db()
93+
obj2.refresh_from_db()
94+
95+
# assert that the 'other' field didn't change
96+
assert obj1.id == obj2.id
97+
assert obj1.other == other_obj
98+
assert obj2.other == other_obj
99+
assert obj1.data == "different data"
100+
assert obj2.data == "different data"
101+
102+
103+
def test_on_conflict_update_foreign_key_by_id():
104+
"""
105+
Tests whether simple upsert works correctly when the conflicting field is a
106+
foreign key specified as an id.
107+
"""
108+
109+
other_model = get_fake_model({})
110+
111+
model = get_fake_model({
112+
'other': models.OneToOneField(
113+
other_model,
114+
on_delete=models.CASCADE,
115+
),
116+
'data': models.CharField(max_length=255),
117+
})
118+
119+
other_obj = other_model.objects.create()
120+
121+
obj1 = (
122+
model.objects
123+
.on_conflict(['other_id'], ConflictAction.UPDATE)
124+
.insert_and_get(other_id=other_obj.pk, data="some data")
125+
)
126+
127+
assert obj1.other == other_obj
128+
assert obj1.data == "some data"
129+
130+
obj1.refresh_from_db()
131+
assert obj1.other == other_obj
132+
assert obj1.data == "some data"
133+
134+
obj2 = (
135+
model.objects
136+
.on_conflict(['other_id'], ConflictAction.UPDATE)
137+
.insert_and_get(other_id=other_obj.pk, data="different data")
138+
)
139+
140+
assert obj2.other == other_obj
141+
assert obj2.data == "different data"
142+
143+
obj1.refresh_from_db()
144+
obj2.refresh_from_db()
145+
146+
# assert that the 'other' field didn't change
147+
assert obj1.id == obj2.id
148+
assert obj1.other == other_obj
149+
assert obj2.other == other_obj
150+
assert obj1.data == "different data"
151+
assert obj2.data == "different data"

0 commit comments

Comments
 (0)