Skip to content

Commit c58761e

Browse files
committed
fix(141): handle after_flush_postexec when creating version objects
when creating version objects if a person has created after_flush_postexec hook, which keeps calling `after_flush` untill it exhausts 100 attempts or session is no longer dirty, this is picked up by mapper as after_update which within same transaction adds a update operation type, so we have a check if target is already in UoW we continue with operation type that it is in untill transaction is completed.
1 parent 5f1e098 commit c58761e

3 files changed

Lines changed: 53 additions & 4 deletions

File tree

sqlalchemy_history/operation.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,11 @@ def add_update(self, target):
9090
del state_copy[rel_key]
9191

9292
if state_copy:
93-
self.add(Operation(target, Operation.UPDATE))
94-
93+
if target in self:
94+
# If already in current transaction and some event hook did a update
95+
# prior to commit hook, continue with operation type as it is
96+
self.add(Operation(target, self[self.format_key(target)].type))
97+
else:
98+
self.add(Operation(target, Operation.UPDATE))
9599
def add_delete(self, target):
96100
self.add(Operation(target, Operation.DELETE))
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import datetime
2+
import sqlalchemy as sa
3+
from copy import copy
4+
5+
from tests import TestCase
6+
from sqlalchemy_history import version_class
7+
8+
9+
class TestBug141(TestCase):
10+
# ref: https://github.com/corridor/sqlalchemy-history/issues/141
11+
def create_models(self):
12+
class Author(self.Model):
13+
__tablename__ = "author"
14+
__versioned__ = copy(self.options)
15+
16+
id = sa.Column(
17+
sa.Integer, sa.Sequence(f"{__tablename__}_seq", start=1), autoincrement=True, primary_key=True
18+
)
19+
name = sa.Column(sa.Unicode(255))
20+
21+
self.Author = Author
22+
23+
def test_add_record(self):
24+
author = self.Author(name="Author 1")
25+
@sa.event.listens_for(self.session, 'after_flush_postexec')
26+
def after_flush_postexec(session, flush_context):
27+
if author.name != "yoyoyoyoyo":
28+
author.name = "yoyoyoyoyo"
29+
self.session.add(author)
30+
self.session.commit()
31+
32+
versioned_objs = self.session.query(version_class(self.Author)).all()
33+
assert len(versioned_objs) == 1
34+
assert versioned_objs[0].operation_type == 0
35+
assert versioned_objs[0].name == "yoyoyoyoyo"
36+
author.name = "sdfeoinfe"
37+
self.session.add(author)
38+
self.session.commit()
39+
versioned_objs = self.session.query(version_class(self.Author)).all()
40+
assert len(versioned_objs) == 2
41+
assert versioned_objs[0].operation_type == 0
42+
assert versioned_objs[1].operation_type == 1
43+
assert versioned_objs[0].name == versioned_objs[1].name == "yoyoyoyoyo"
44+
sa.event.remove(self.session, "after_flush_postexec", after_flush_postexec)

tests/test_exotic_operation_combos.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ def test_replace_deleted_object_with_update(self):
5757

5858
self.session.delete(article)
5959
self.session.flush()
60-
61-
article2.id = article.id
60+
# we were earlier updating ID which didn't seem right, so changed this to name since
61+
# id is used by us for identity in operation
62+
article2.name = article.name
6263
self.session.commit()
6364
assert article2.versions.count() == 2
6465
assert article2.versions[0].operation_type == Operation.INSERT

0 commit comments

Comments
 (0)