On Sat, Jan 27, 2018 at 11:54 PM, <raj.abhila...@gmail.com> wrote: > Hi Mike, > > I am one of the other Mailman developers, just following up on behalf of > Mark. > > On Sunday, 21 January 2018 06:51:29 UTC-8, Mike Bayer wrote: >> >> here are a few questions: >> >> 1. is mailman using the version_id mapper feature with these mappings ? > > > No, Mailman isn't using version_id mapper feature. > >> >> >> 2. do mailman mappings use natural primary keys? in the above example >> are the primary keys of objects changing? >> > No, the primary keys of the objects aren't changing. > > I tried to run the test with echo='debug' to produce SQL statements emitted > and found something odd. I see that the UPDATE statement from > post_update=True is being emitted *after* the DELETE statement, when trying > to delete a record. This causes the UPDATE statement to error out due to > non-existent row. This happens both in 1.1 and 1.2, although 1.1 passes it > silently while 1.2 errors out with the stack trace that Mark presented > above. > > Here is the relevant SQL statements: > > User.id == 2 is being accessed after it has been deleted. > > 1.1.15: > > > 018-01-27 20:32:54,580 INFO sqlalchemy.engine.base.Engine DELETE FROM user > WHERE user.id = ? > 2018-01-27 20:32:54,580 INFO sqlalchemy.engine.base.Engine (2,) > 2018-01-27 20:32:54,581 INFO sqlalchemy.engine.base.Engine UPDATE user SET > _preferred_address_id=? WHERE user.id = ? > 2018-01-27 20:32:54,581 INFO sqlalchemy.engine.base.Engine (None, 2) > 2018-01-27 20:32:54,582 INFO sqlalchemy.engine.base.Engine DELETE FROM > preferences WHERE preferences.id = ? > 2018-01-27 20:32:54,582 INFO sqlalchemy.engine.base.Engine (4,) > 2018-01-27 20:32:54,583 INFO sqlalchemy.engine.base.Engine SELECT > user.password AS user_password, user.id AS user_id, user.display_name AS > user_display_name, user._user_id AS user__user_id, user._created_on AS > user__created_on, user.is_server_owner AS user_is_server_owner, > user._preferred_address_id AS user__preferred_address_id, > user.preferences_id AS user_preferences_id > FROM user ORDER BY user.id > 2018-01-27 20:32:54,584 INFO sqlalchemy.engine.base.Engine () > > 1.2.2: > > > 2018-01-27 20:26:58,981 INFO sqlalchemy.engine.base.Engine DELETE FROM user > WHERE user.id = ? > 2018-01-27 20:26:58,982 INFO sqlalchemy.engine.base.Engine (2,) > 2018-01-27 20:26:58,982 INFO sqlalchemy.engine.base.Engine UPDATE user SET > _preferred_address_id=? WHERE user.id = ? > 2018-01-27 20:26:58,983 INFO sqlalchemy.engine.base.Engine (None, 2) > 2018-01-27 20:26:58,983 INFO sqlalchemy.engine.base.Engine ROLLBACK > sqlalchemy.orm.exc.StaleDataError: UPDATE statement on table 'user' expected > to update 1 row(s); 0 were matched. > > > > This[1] is the 'user' model definition and the foreign key that should be > dis-connected before the DELETE, as is my understanding at least.
Well, i just recreated your User/ Address/ Preferences model and tried every combination of deleting / removing Address, Preference, and User objects / attributes using a loop with flags iterating through 1024 different permutations, and I can't reproduce that condition, the post update always does the right thing. From what we see, we know that the User object is definitely subject to a session.delete(), but as far as other objects, it's not clear yet. So, need to know a lot more detail: 1. what is the *full* SQL output of every row being altered within this test 2. are you manipulating the integer foreign key attributes of objects? E.g. user.preferred_address_id = None? Are you manipulating relationship-bound attributes? e.g. user.preferred_address = None ? are the two approaches being combined? 3. can you place a session.flush() right before the Query in question so that the flush simply proceeds 4. right before this flush and/or Query proceeds, what are the contents of: session.deleted, session.dirty, session.new ? 5. are relationships being reassigned? like user1.addresses.append(user2.address[0]) ? Alternatively: Below is a sample of the relevant bits of the model in isolation, with a delete of a User and Preferences object. if you could make this test show the problem, that would be the most effective way to immediately show what's happening. from sqlalchemy import * from sqlalchemy.orm import * from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class User(Base): __tablename__ = 'user' id = Column(Integer, primary_key=True) favorite_address_id = Column(ForeignKey('address.id')) addresses = relationship("Address", backref="user", primaryjoin=lambda: User.id == Address.user_id) favorite_address = relationship( "Address", post_update=True, primaryjoin=lambda: User.favorite_address_id == Address.id) preferences_id = Column(Integer, ForeignKey('preferences.id'), index=True) preferences = relationship( 'Preferences', backref=backref('user', uselist=False)) class Preferences(Base): __tablename__ = 'preferences' id = Column(Integer, primary_key=True) class Address(Base): __tablename__ = 'address' id = Column(Integer, primary_key=True) user_id = Column(ForeignKey('user.id')) e = create_engine("sqlite://", echo=True) s = Session(e) Base.metadata.create_all(e) p1 = Preferences() a1, a2, a3 = Address(), Address(), Address() u1 = User(addresses=[a1, a2, a3], favorite_address=a2, preferences=p1) s.add(u1) s.commit() s.delete(p1) s.delete(u1) s.commit() > > [1]: > https://gitlab.com/mailman/mailman/blob/master/src/mailman/model/user.py#L68 > > Please let me know if you need any more information about this. > > thanks, > Abhilash > > > >> >> >> >> On Sun, Jan 21, 2018 at 8:24 AM, Mike Bayer <mik...@zzzcomputing.com> >> wrote: >> > On Sat, Jan 20, 2018 at 8:19 PM, Mark Sapiro <ma...@msapiro.net> wrote: >> >> Gnu Mailman 3 uses SQL Alchemy to access SQL databases. We have an >> >> issue >> >> with one of or unit tests which passes with SQLAlchemy <= 1.1.15 but >> >> which >> >> fails with SQLAlchemy >= 1.2. The failure is independent of the backend >> >> (sglite, mysql or pgsql). Here's a traceback from the failure. >> >> >> >> Traceback (most recent call last): >> >> File "/builds/mailman/mailman/src/mailman/model/tests/test_user.py", >> >> line >> >> 184, in test_absorb_memberships >> >> all_users = list(self._manager.users) >> >> File "/builds/mailman/mailman/src/mailman/model/usermanager.py", line >> >> 102, >> >> in users >> >> yield from store.query(User).order_by(User.id).all() >> >> File >> >> >> >> "/builds/mailman/mailman/.tox/py36-nocov/lib/python3.6/site-packages/sqlalchemy/orm/query.py", >> >> line 2726, in all >> >> return list(self) >> >> File >> >> >> >> "/builds/mailman/mailman/.tox/py36-nocov/lib/python3.6/site-packages/sqlalchemy/orm/query.py", >> >> line 2877, in __iter__ >> >> self.session._autoflush() >> >> File >> >> >> >> "/builds/mailman/mailman/.tox/py36-nocov/lib/python3.6/site-packages/sqlalchemy/orm/session.py", >> >> line 1434, in _autoflush >> >> self.flush() >> >> File >> >> >> >> "/builds/mailman/mailman/.tox/py36-nocov/lib/python3.6/site-packages/sqlalchemy/orm/session.py", >> >> line 2243, in flush >> >> self._flush(objects) >> >> File >> >> >> >> "/builds/mailman/mailman/.tox/py36-nocov/lib/python3.6/site-packages/sqlalchemy/orm/session.py", >> >> line 2369, in _flush >> >> transaction.rollback(_capture_exception=True) >> >> File >> >> >> >> "/builds/mailman/mailman/.tox/py36-nocov/lib/python3.6/site-packages/sqlalchemy/util/langhelpers.py", >> >> line 66, in __exit__ >> >> compat.reraise(exc_type, exc_value, exc_tb) >> >> File >> >> >> >> "/builds/mailman/mailman/.tox/py36-nocov/lib/python3.6/site-packages/sqlalchemy/util/compat.py", >> >> line 187, in reraise >> >> raise value >> >> File >> >> >> >> "/builds/mailman/mailman/.tox/py36-nocov/lib/python3.6/site-packages/sqlalchemy/orm/session.py", >> >> line 2333, in _flush >> >> flush_context.execute() >> >> File >> >> >> >> "/builds/mailman/mailman/.tox/py36-nocov/lib/python3.6/site-packages/sqlalchemy/orm/unitofwork.py", >> >> line 391, in execute >> >> rec.execute(self) >> >> File >> >> >> >> "/builds/mailman/mailman/.tox/py36-nocov/lib/python3.6/site-packages/sqlalchemy/orm/unitofwork.py", >> >> line 542, in execute >> >> persistence.post_update(self.mapper, states, uow, cols) >> >> File >> >> >> >> "/builds/mailman/mailman/.tox/py36-nocov/lib/python3.6/site-packages/sqlalchemy/orm/persistence.py", >> >> line 234, in post_update >> >> mapper, table, update) >> >> File >> >> >> >> "/builds/mailman/mailman/.tox/py36-nocov/lib/python3.6/site-packages/sqlalchemy/orm/persistence.py", >> >> line 982, in _emit_post_update_statements >> >> (table.description, len(records), rows)) >> >> sqlalchemy.orm.exc.StaleDataError: UPDATE statement on table 'user' >> >> expected >> >> to update 1 row(s); 0 were matched. >> > >> > >> > so StaleDataError is thrown when the ORM seeks to update or delete a >> > row based on primary key, and the row which that statement would match >> > is no longer present. This is essentially an assertion that the state >> > of the database is what the ORM expects based on its internal state. >> > The "stale" name refers to the typical case that some other >> > transaction has changed the state of the database while our >> > transaction is proceeding, however it is often the case that some >> > combination of how the mappings are set up combined with what the >> > application is doing causes the ORM to not have a row available which >> > should. >> > >> > In this case, the operation is specifically in relationship to the >> > usage of the "post_update" feature of mappings >> > >> > (http://docs.sqlalchemy.org/en/latest/orm/relationship_persistence.html#post-update) >> > , since we can see that in the stack trace. The difference between >> > 1.1 and 1.2 is that the StaleDataError assertions have been added to >> > the post update system: >> > >> > http://docs.sqlalchemy.org/en/latest/changelog/migration_12.html#post-update-integrates-with-orm-versioning >> > which is almost definitely part of the reason you get an error in 1.2 >> > and not in 1.1. >> > >> > What remains to be understood is whether your test produced an UPDATE >> > that didn't match a row in any case, and in 1.2, we just happen to be >> > raising an error for it, whereas under 1.1 this error condition >> > silently passed. Or, if the change in 1.2 itself has a regression and >> > the steps / SQL emitted are erroneously different. >> > >> > Things that would help us to see would be: 1. a look at the models >> > in question and how post_update is being set up and 2. when the >> > test runs, grab the full SQL output, using something like echo='debug' >> > on create_engine() or logging the "sqlalchemy.engine" logger. We >> > want to look at the exact sequence of SQL emitted both for the 1.1 >> > series and the 1.2 series, to see what the nature of the UPDATE >> > against a missing row is. If it looks like things work exactly the >> > same in 1.1 then that means your post_update has been missing this row >> > the whole time. >> > >> > This is just the top level information, I haven't looked at your >> > actual code yet just the stack trace. I'm not available today but >> > later in the week if you can produce self-contained reproducing >> > examples then I can further examine if we have a regression on the >> > SQLAlchemy side, or if this is expected behavior. >> > >> > >> > >> > >> > >> > >> >> >> >> >> >> The test that fails sets up 3 mailing lists and subscribes 2 users >> >> (Anne and >> >> Bart) to some or all of the lists with different roles. Then user Anne >> >> "absorbs" user Bart so user Bart is deleted and user Anne assumes all >> >> Bart's >> >> memberships. The actual code in the test may not be intelligible >> >> without >> >> more Mailman knowledge, but it is: >> >> def test_absorb_memberships(self): >> >> # When a user is absorbed, all of their user-subscribed >> >> memberships >> >> # are relinked to the absorbing user. >> >> mlist2 = create_list('te...@example.com') >> >> mlist3 = create_list('te...@example.com') >> >> with transaction(): >> >> # This has to happen in a transaction so that both the user >> >> and >> >> # the preferences objects get valid ids. >> >> bart = self._manager.create_user('b...@example.com', 'Bart >> >> Person') >> >> set_preferred(bart) >> >> # Subscribe both users to self._mlist. >> >> self._mlist.subscribe(self._anne, MemberRole.member) >> >> self._mlist.subscribe(bart, MemberRole.moderator) >> >> # Subscribe only Bart to mlist2. >> >> mlist2.subscribe(bart, MemberRole.owner) >> >> # Subscribe only Bart's address to mlist3. >> >> mlist3.subscribe(bart.preferred_address, MemberRole.moderator) >> >> # There are now 4 memberships, one with Anne two with Bart's >> >> user >> >> and >> >> # one with Bart's address. >> >> all_members = list(self._manager.members) >> >> self.assertEqual(len(all_members), 4, all_members) >> >> # Do the absorption. >> >> self._anne.absorb(bart) >> >> # The Bart user has been deleted, leaving only the Anne user in >> >> the >> >> # user manager. >> >> all_users = list(self._manager.users) >> >> self.assertEqual(len(all_users), 1) >> >> self.assertEqual(all_users[0], self._anne) >> >> # There are no leftover memberships for user Bart. Anne owns >> >> all >> >> the >> >> # memberships. >> >> all_members = list(self._manager.members) >> >> self.assertEqual(len(all_members), 4, all_members) >> >> self.assertEqual(self._anne.memberships.member_count, 4) >> >> memberships = {(member.list_id, member.role): member >> >> for member in self._anne.memberships.members} >> >> # Note that Anne is now both a member and moderator of the test >> >> list. >> >> self.assertEqual(set(memberships), set([ >> >> ('test.example.com', MemberRole.member), >> >> ('test.example.com', MemberRole.moderator), >> >> ('test2.example.com', MemberRole.owner), >> >> ('test3.example.com', MemberRole.moderator), >> >> ])) >> >> # Both of Bart's previous user subscriptions are now >> >> transferred to >> >> # the Anne user. >> >> self.assertEqual( >> >> memberships[('test.example.com', >> >> MemberRole.moderator)].address, >> >> self._anne.preferred_address) >> >> self.assertEqual( >> >> memberships[('test2.example.com', >> >> MemberRole.owner)].address, >> >> self._anne.preferred_address) >> >> # Bart's address was subscribed; it must not have been changed. >> >> Of >> >> # course, Anne now controls ba...@example.com. >> >> key = ('test3.example.com', MemberRole.moderator) >> >> self.assertEqual(memberships[key].address.email, >> >> 'ba...@example.com') >> >> self.assertEqual(self._manager.get_user('ba...@example.com'), >> >> self._anne) >> >> >> >> >> >> >> >> The start of the traceback is the statement >> >> # The Bart user has been deleted, leaving only the Anne user in >> >> the >> >> # user manager. >> >> all_users = list(self._manager.users) >> >> Versions older than 1.2 do not throw the >> >> sqlalchemy.orm.exc.StaleDataError >> >> exception at this point. >> >> >> >> If I attempt to fix the test by doing the `self._anne.absorb(bart)` in >> >> a >> >> transaction that gets committed on completion, the same exception is >> >> thrown >> >> on the commit. >> >> >> >> Does anyone have any ideas about this? >> >> >> >> -- >> >> SQLAlchemy - >> >> The Python SQL Toolkit and Object Relational Mapper >> >> >> >> http://www.sqlalchemy.org/ >> >> >> >> To post example code, please provide an MCVE: Minimal, Complete, and >> >> Verifiable Example. See http://stackoverflow.com/help/mcve for a full >> >> description. >> >> --- >> >> You received this message because you are subscribed to the Google >> >> Groups >> >> "sqlalchemy" group. >> >> To unsubscribe from this group and stop receiving emails from it, send >> >> an >> >> email to sqlalchemy+...@googlegroups.com. >> >> To post to this group, send email to sqlal...@googlegroups.com. >> >> Visit this group at https://groups.google.com/group/sqlalchemy. >> >> For more options, visit https://groups.google.com/d/optout. > > -- > SQLAlchemy - > The Python SQL Toolkit and Object Relational Mapper > > http://www.sqlalchemy.org/ > > To post example code, please provide an MCVE: Minimal, Complete, and > Verifiable Example. See http://stackoverflow.com/help/mcve for a full > description. > --- > You received this message because you are subscribed to the Google Groups > "sqlalchemy" group. > To unsubscribe from this group and stop receiving emails from it, send an > email to sqlalchemy+unsubscr...@googlegroups.com. > To post to this group, send email to sqlalchemy@googlegroups.com. > Visit this group at https://groups.google.com/group/sqlalchemy. > For more options, visit https://groups.google.com/d/optout. -- SQLAlchemy - The Python SQL Toolkit and Object Relational Mapper http://www.sqlalchemy.org/ To post example code, please provide an MCVE: Minimal, Complete, and Verifiable Example. See http://stackoverflow.com/help/mcve for a full description. --- You received this message because you are subscribed to the Google Groups "sqlalchemy" group. To unsubscribe from this group and stop receiving emails from it, send an email to sqlalchemy+unsubscr...@googlegroups.com. To post to this group, send email to sqlalchemy@googlegroups.com. Visit this group at https://groups.google.com/group/sqlalchemy. For more options, visit https://groups.google.com/d/optout.