From: Mike Bayer Date: Thu, 7 Apr 2011 16:49:29 +0000 (-0400) Subject: - Some fixes to "evaulate" and "fetch" evaluation X-Git-Tag: rel_0_7b4~32 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=708a25e76a3cb9528c65d45ad37fc562cf178e44;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - Some fixes to "evaulate" and "fetch" evaluation when query.update(), query.delete() are called. The retrieval of records is done after autoflush in all cases, and before update/delete is emitted, guarding against unflushed data present as well as expired objects failing during the evaluation. [ticket:2122] --- diff --git a/CHANGES b/CHANGES index 6a2f45390b..c71aa03d5a 100644 --- a/CHANGES +++ b/CHANGES @@ -30,6 +30,14 @@ CHANGES implementation/behavior are present. - orm + - Some fixes to "evaulate" and "fetch" evaluation + when query.update(), query.delete() are called. + The retrieval of records is done after autoflush + in all cases, and before update/delete is + emitted, guarding against unflushed data present + as well as expired objects failing during + the evaluation. [ticket:2122] + - Reworded the exception raised when a flush is attempted of a subclass that is not polymorphic against the supertype. [ticket:2063] diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index dcfbf3b32f..53478c9c66 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -2138,6 +2138,9 @@ class Query(object): session = self.session + if self._autoflush: + session._autoflush() + if synchronize_session == 'evaluate': try: evaluator_compiler = evaluator.EvaluatorCompiler() @@ -2154,9 +2157,16 @@ class Query(object): "Specify 'fetch' or False for the synchronize_session " "parameter.") - delete_stmt = sql.delete(primary_table, context.whereclause) + target_cls = self._mapper_zero().class_ + + #TODO: detect when the where clause is a trivial primary key match + objs_to_expunge = [ + obj for (cls, pk),obj in + session.identity_map.iteritems() + if issubclass(cls, target_cls) and + eval_condition(obj)] - if synchronize_session == 'fetch': + elif synchronize_session == 'fetch': #TODO: use RETURNING when available select_stmt = context.statement.with_only_columns( primary_table.primary_key) @@ -2164,19 +2174,11 @@ class Query(object): select_stmt, params=self._params).fetchall() - if self._autoflush: - session._autoflush() + delete_stmt = sql.delete(primary_table, context.whereclause) + result = session.execute(delete_stmt, params=self._params) if synchronize_session == 'evaluate': - target_cls = self._mapper_zero().class_ - - #TODO: detect when the where clause is a trivial primary key match - objs_to_expunge = [ - obj for (cls, pk),obj in - session.identity_map.iteritems() - if issubclass(cls, target_cls) and - eval_condition(obj)] for obj in objs_to_expunge: session._remove_newly_deleted(attributes.instance_state(obj)) elif synchronize_session == 'fetch': @@ -2271,6 +2273,9 @@ class Query(object): session = self.session + if self._autoflush: + session._autoflush() + if synchronize_session == 'evaluate': try: evaluator_compiler = evaluator.EvaluatorCompiler() @@ -2291,43 +2296,45 @@ class Query(object): "Could not evaluate current criteria in Python. " "Specify 'fetch' or False for the " "synchronize_session parameter.") + target_cls = self._mapper_zero().class_ + matched_objects = [] + for (cls, pk),obj in session.identity_map.iteritems(): + evaluated_keys = value_evaluators.keys() - update_stmt = sql.update(primary_table, context.whereclause, values) + if issubclass(cls, target_cls) and eval_condition(obj): + matched_objects.append(obj) - if synchronize_session == 'fetch': + elif synchronize_session == 'fetch': select_stmt = context.statement.with_only_columns( primary_table.primary_key) matched_rows = session.execute( select_stmt, params=self._params).fetchall() - if self._autoflush: - session._autoflush() + update_stmt = sql.update(primary_table, context.whereclause, values) + result = session.execute(update_stmt, params=self._params) if synchronize_session == 'evaluate': target_cls = self._mapper_zero().class_ - for (cls, pk),obj in session.identity_map.iteritems(): - evaluated_keys = value_evaluators.keys() + for obj in matched_objects: + state, dict_ = attributes.instance_state(obj),\ + attributes.instance_dict(obj) - if issubclass(cls, target_cls) and eval_condition(obj): - state, dict_ = attributes.instance_state(obj),\ - attributes.instance_dict(obj) - - # only evaluate unmodified attributes - to_evaluate = state.unmodified.intersection( - evaluated_keys) - for key in to_evaluate: - dict_[key] = value_evaluators[key](obj) - - state.commit(dict_, list(to_evaluate)) - - # expire attributes with pending changes - # (there was no autoflush, so they are overwritten) - state.expire_attributes(dict_, - set(evaluated_keys). - difference(to_evaluate)) + # only evaluate unmodified attributes + to_evaluate = state.unmodified.intersection( + evaluated_keys) + for key in to_evaluate: + dict_[key] = value_evaluators[key](obj) + + state.commit(dict_, list(to_evaluate)) + + # expire attributes with pending changes + # (there was no autoflush, so they are overwritten) + state.expire_attributes(dict_, + set(evaluated_keys). + difference(to_evaluate)) elif synchronize_session == 'fetch': target_mapper = self._mapper_zero() diff --git a/test/orm/test_query.py b/test/orm/test_query.py index bcb82a9afb..946f24a5d1 100644 --- a/test/orm/test_query.py +++ b/test/orm/test_query.py @@ -1790,368 +1790,6 @@ class ImmediateTest(_fixtures.FixtureTest): sess.bind = testing.db eq_(sess.query().value(sa.literal_column('1').label('x')), 1) -class UpdateDeleteTest(fixtures.MappedTest): - @classmethod - def define_tables(cls, metadata): - Table('users', metadata, - Column('id', Integer, primary_key=True, test_needs_autoincrement=True), - Column('name', String(32)), - Column('age', Integer)) - - Table('documents', metadata, - Column('id', Integer, primary_key=True, test_needs_autoincrement=True), - Column('user_id', None, ForeignKey('users.id')), - Column('title', String(32))) - - @classmethod - def setup_classes(cls): - class User(cls.Comparable): - pass - - class Document(cls.Comparable): - pass - - @classmethod - def insert_data(cls): - users = cls.tables.users - - users.insert().execute([ - dict(id=1, name='john', age=25), - dict(id=2, name='jack', age=47), - dict(id=3, name='jill', age=29), - dict(id=4, name='jane', age=37), - ]) - - def insert_documents(self): - documents = self.tables.documents - - documents.insert().execute([ - dict(id=1, user_id=1, title='foo'), - dict(id=2, user_id=1, title='bar'), - dict(id=3, user_id=2, title='baz'), - ]) - - @classmethod - def setup_mappers(cls): - documents, Document, User, users = (cls.tables.documents, - cls.classes.Document, - cls.classes.User, - cls.tables.users) - - mapper(User, users) - mapper(Document, documents, properties={ - 'user': relationship(User, lazy='joined', - backref=backref('documents', lazy='select')) - }) - - def test_illegal_operations(self): - User = self.classes.User - - s = create_session() - - for q, mname in ( - (s.query(User).limit(2), "limit"), - (s.query(User).offset(2), "offset"), - (s.query(User).limit(2).offset(2), "limit"), - (s.query(User).order_by(User.id), "order_by"), - (s.query(User).group_by(User.id), "group_by"), - (s.query(User).distinct(), "distinct") - ): - assert_raises_message(sa_exc.InvalidRequestError, r"Can't call Query.update\(\) when %s\(\) has been called" % mname, q.update, {'name':'ed'}) - assert_raises_message(sa_exc.InvalidRequestError, r"Can't call Query.delete\(\) when %s\(\) has been called" % mname, q.delete) - - - def test_delete(self): - User = self.classes.User - - sess = create_session(bind=testing.db, autocommit=False) - - john,jack,jill,jane = sess.query(User).order_by(User.id).all() - sess.query(User).filter(or_(User.name == 'john', User.name == 'jill')).delete() - - assert john not in sess and jill not in sess - - eq_(sess.query(User).order_by(User.id).all(), [jack,jane]) - - def test_delete_with_bindparams(self): - User = self.classes.User - - sess = create_session(bind=testing.db, autocommit=False) - - john,jack,jill,jane = sess.query(User).order_by(User.id).all() - sess.query(User).filter('name = :name').params(name='john').delete('fetch') - assert john not in sess - - eq_(sess.query(User).order_by(User.id).all(), [jack,jill,jane]) - - def test_delete_rollback(self): - User = self.classes.User - - sess = sessionmaker()() - john,jack,jill,jane = sess.query(User).order_by(User.id).all() - sess.query(User).filter(or_(User.name == 'john', User.name == 'jill')).delete(synchronize_session='evaluate') - assert john not in sess and jill not in sess - sess.rollback() - assert john in sess and jill in sess - - def test_delete_rollback_with_fetch(self): - User = self.classes.User - - sess = sessionmaker()() - john,jack,jill,jane = sess.query(User).order_by(User.id).all() - sess.query(User).filter(or_(User.name == 'john', User.name == 'jill')).delete(synchronize_session='fetch') - assert john not in sess and jill not in sess - sess.rollback() - assert john in sess and jill in sess - - def test_delete_without_session_sync(self): - User = self.classes.User - - sess = create_session(bind=testing.db, autocommit=False) - - john,jack,jill,jane = sess.query(User).order_by(User.id).all() - sess.query(User).filter(or_(User.name == 'john', User.name == 'jill')).delete(synchronize_session=False) - - assert john in sess and jill in sess - - eq_(sess.query(User).order_by(User.id).all(), [jack,jane]) - - def test_delete_with_fetch_strategy(self): - User = self.classes.User - - sess = create_session(bind=testing.db, autocommit=False) - - john,jack,jill,jane = sess.query(User).order_by(User.id).all() - sess.query(User).filter(or_(User.name == 'john', User.name == 'jill')).delete(synchronize_session='fetch') - - assert john not in sess and jill not in sess - - eq_(sess.query(User).order_by(User.id).all(), [jack,jane]) - - @testing.fails_on('mysql', 'FIXME: unknown') - def test_delete_invalid_evaluation(self): - User = self.classes.User - - sess = create_session(bind=testing.db, autocommit=False) - - john,jack,jill,jane = sess.query(User).order_by(User.id).all() - - assert_raises(sa_exc.InvalidRequestError, - sess.query(User).filter(User.name == select([func.max(User.name)])).delete, synchronize_session='evaluate' - ) - - sess.query(User).filter(User.name == select([func.max(User.name)])).delete(synchronize_session='fetch') - - assert john not in sess - - eq_(sess.query(User).order_by(User.id).all(), [jack,jill,jane]) - - def test_update(self): - User, users = self.classes.User, self.tables.users - - sess = create_session(bind=testing.db, autocommit=False) - - john,jack,jill,jane = sess.query(User).order_by(User.id).all() - sess.query(User).filter(User.age > 29).update({'age': User.age - 10}, synchronize_session='evaluate') - - eq_([john.age, jack.age, jill.age, jane.age], [25,37,29,27]) - eq_(sess.query(User.age).order_by(User.id).all(), zip([25,37,29,27])) - - sess.query(User).filter(User.age > 29).update({User.age: User.age - 10}, synchronize_session='evaluate') - eq_([john.age, jack.age, jill.age, jane.age], [25,27,29,27]) - eq_(sess.query(User.age).order_by(User.id).all(), zip([25,27,29,27])) - - sess.query(User).filter(User.age > 27).update({users.c.age: User.age - 10}, synchronize_session='evaluate') - eq_([john.age, jack.age, jill.age, jane.age], [25,27,19,27]) - eq_(sess.query(User.age).order_by(User.id).all(), zip([25,27,19,27])) - - sess.query(User).filter(User.age == 25).update({User.age: User.age - 10}, synchronize_session='fetch') - eq_([john.age, jack.age, jill.age, jane.age], [15,27,19,27]) - eq_(sess.query(User.age).order_by(User.id).all(), zip([15,27,19,27])) - - @testing.provide_metadata - def test_update_attr_names(self): - metadata = self.metadata - data = Table('data', metadata, - Column('id', Integer, primary_key=True, test_needs_autoincrement=True), - Column('counter', Integer, nullable=False, default=0) - ) - class Data(fixtures.ComparableEntity): - pass - - mapper(Data, data, properties={'cnt':data.c.counter}) - metadata.create_all() - d1 = Data() - sess = Session() - sess.add(d1) - sess.commit() - eq_(d1.cnt, 0) - - sess.query(Data).update({Data.cnt:Data.cnt + 1}) - sess.flush() - - eq_(d1.cnt, 1) - - sess.query(Data).update({Data.cnt:Data.cnt + 1}, 'fetch') - sess.flush() - - eq_(d1.cnt, 2) - sess.close() - - def test_update_with_bindparams(self): - User = self.classes.User - - sess = create_session(bind=testing.db, autocommit=False) - - john,jack,jill,jane = sess.query(User).order_by(User.id).all() - - sess.query(User).filter('age > :x').params(x=29).update({'age': User.age - 10}, synchronize_session='fetch') - - eq_([john.age, jack.age, jill.age, jane.age], [25,37,29,27]) - eq_(sess.query(User.age).order_by(User.id).all(), zip([25,37,29,27])) - - def test_update_changes_resets_dirty(self): - User = self.classes.User - - sess = create_session(bind=testing.db, autocommit=False, autoflush=False) - - john,jack,jill,jane = sess.query(User).order_by(User.id).all() - - john.age = 50 - jack.age = 37 - - # autoflush is false. therefore our '50' and '37' are getting blown away by this operation. - - sess.query(User).filter(User.age > 29).update({'age': User.age - 10}, synchronize_session='evaluate') - - for x in (john, jack, jill, jane): - assert not sess.is_modified(x) - - eq_([john.age, jack.age, jill.age, jane.age], [25,37,29,27]) - - john.age = 25 - assert john in sess.dirty - assert jack in sess.dirty - assert jill not in sess.dirty - assert not sess.is_modified(john) - assert not sess.is_modified(jack) - - def test_update_changes_with_autoflush(self): - User = self.classes.User - - sess = create_session(bind=testing.db, autocommit=False, autoflush=True) - - john,jack,jill,jane = sess.query(User).order_by(User.id).all() - - john.age = 50 - jack.age = 37 - - sess.query(User).filter(User.age > 29).update({'age': User.age - 10}, synchronize_session='evaluate') - - for x in (john, jack, jill, jane): - assert not sess.is_modified(x) - - eq_([john.age, jack.age, jill.age, jane.age], [40, 27, 29, 27]) - - john.age = 25 - assert john in sess.dirty - assert jack not in sess.dirty - assert jill not in sess.dirty - assert sess.is_modified(john) - assert not sess.is_modified(jack) - - - - def test_update_with_expire_strategy(self): - User = self.classes.User - - sess = create_session(bind=testing.db, autocommit=False) - - john,jack,jill,jane = sess.query(User).order_by(User.id).all() - sess.query(User).filter(User.age > 29).update({'age': User.age - 10}, synchronize_session='fetch') - - eq_([john.age, jack.age, jill.age, jane.age], [25,37,29,27]) - eq_(sess.query(User.age).order_by(User.id).all(), zip([25,37,29,27])) - - @testing.fails_if(lambda: not testing.db.dialect.supports_sane_rowcount) - def test_update_returns_rowcount(self): - User = self.classes.User - - sess = create_session(bind=testing.db, autocommit=False) - - rowcount = sess.query(User).filter(User.age > 29).update({'age': User.age + 0}) - eq_(rowcount, 2) - - rowcount = sess.query(User).filter(User.age > 29).update({'age': User.age - 10}) - eq_(rowcount, 2) - - @testing.fails_if(lambda: not testing.db.dialect.supports_sane_rowcount) - def test_delete_returns_rowcount(self): - User = self.classes.User - - sess = create_session(bind=testing.db, autocommit=False) - - rowcount = sess.query(User).filter(User.age > 26).delete(synchronize_session=False) - eq_(rowcount, 3) - - def test_update_with_eager_relationships(self): - Document = self.classes.Document - - self.insert_documents() - - sess = create_session(bind=testing.db, autocommit=False) - - foo,bar,baz = sess.query(Document).order_by(Document.id).all() - sess.query(Document).filter(Document.user_id == 1).update({'title': Document.title+Document.title}, synchronize_session='fetch') - - eq_([foo.title, bar.title, baz.title], ['foofoo','barbar', 'baz']) - eq_(sess.query(Document.title).order_by(Document.id).all(), zip(['foofoo','barbar', 'baz'])) - - def test_update_with_explicit_joinedload(self): - User = self.classes.User - - sess = create_session(bind=testing.db, autocommit=False) - - john,jack,jill,jane = sess.query(User).order_by(User.id).all() - sess.query(User).options(joinedload(User.documents)).filter(User.age > 29).update({'age': User.age - 10}, synchronize_session='fetch') - - eq_([john.age, jack.age, jill.age, jane.age], [25,37,29,27]) - eq_(sess.query(User.age).order_by(User.id).all(), zip([25,37,29,27])) - - def test_delete_with_eager_relationships(self): - Document = self.classes.Document - - self.insert_documents() - - sess = create_session(bind=testing.db, autocommit=False) - - sess.query(Document).filter(Document.user_id == 1).delete(synchronize_session=False) - - eq_(sess.query(Document.title).all(), zip(['baz'])) - - def test_update_all(self): - User = self.classes.User - - sess = create_session(bind=testing.db, autocommit=False) - - john,jack,jill,jane = sess.query(User).order_by(User.id).all() - sess.query(User).update({'age': 42}, synchronize_session='evaluate') - - eq_([john.age, jack.age, jill.age, jane.age], [42,42,42,42]) - eq_(sess.query(User.age).order_by(User.id).all(), zip([42,42,42,42])) - - def test_delete_all(self): - User = self.classes.User - - sess = create_session(bind=testing.db, autocommit=False) - - john,jack,jill,jane = sess.query(User).order_by(User.id).all() - sess.query(User).delete(synchronize_session='evaluate') - - assert not (john in sess or jack in sess or jill in sess or jane in sess) - eq_(sess.query(User).count(), 0) - - class StatementOptionsTest(QueryTest): def test_query_with_statement_option(self): diff --git a/test/orm/test_update_delete.py b/test/orm/test_update_delete.py new file mode 100644 index 0000000000..d31d272a98 --- /dev/null +++ b/test/orm/test_update_delete.py @@ -0,0 +1,506 @@ +from test.lib.testing import eq_, assert_raises, assert_raises_message +from test.lib import fixtures, testing +from sqlalchemy import Integer, String, ForeignKey, or_, and_, exc, select, func +from sqlalchemy.orm import mapper, relationship, backref, Session, joinedload + +from test.lib.schema import Table, Column + +class UpdateDeleteTest(fixtures.MappedTest): + @classmethod + def define_tables(cls, metadata): + Table('users', metadata, + Column('id', Integer, primary_key=True, + test_needs_autoincrement=True), + Column('name', String(32)), + Column('age', Integer)) + + Table('documents', metadata, + Column('id', Integer, primary_key=True, + test_needs_autoincrement=True), + Column('user_id', None, ForeignKey('users.id')), + Column('title', String(32))) + + @classmethod + def setup_classes(cls): + class User(cls.Comparable): + pass + + class Document(cls.Comparable): + pass + + @classmethod + def insert_data(cls): + users = cls.tables.users + + users.insert().execute([ + dict(id=1, name='john', age=25), + dict(id=2, name='jack', age=47), + dict(id=3, name='jill', age=29), + dict(id=4, name='jane', age=37), + ]) + + documents = cls.tables.documents + + documents.insert().execute([ + dict(id=1, user_id=1, title='foo'), + dict(id=2, user_id=1, title='bar'), + dict(id=3, user_id=2, title='baz'), + ]) + + @classmethod + def setup_mappers(cls): + documents, Document, User, users = (cls.tables.documents, + cls.classes.Document, + cls.classes.User, + cls.tables.users) + + mapper(User, users) + mapper(Document, documents, properties={ + 'user': relationship(User, lazy='joined', + backref=backref('documents', lazy='select')) + }) + + def test_illegal_operations(self): + User = self.classes.User + + s = Session() + + for q, mname in ( + (s.query(User).limit(2), "limit"), + (s.query(User).offset(2), "offset"), + (s.query(User).limit(2).offset(2), "limit"), + (s.query(User).order_by(User.id), "order_by"), + (s.query(User).group_by(User.id), "group_by"), + (s.query(User).distinct(), "distinct") + ): + assert_raises_message( + exc.InvalidRequestError, + r"Can't call Query.update\(\) when %s\(\) has been called" % mname, + q.update, + {'name':'ed'}) + assert_raises_message( + exc.InvalidRequestError, + r"Can't call Query.delete\(\) when %s\(\) has been called" % mname, + q.delete) + + + def test_delete(self): + User = self.classes.User + + sess = Session() + + john,jack,jill,jane = sess.query(User).order_by(User.id).all() + sess.query(User).filter(or_(User.name == 'john', User.name == 'jill')).delete() + + assert john not in sess and jill not in sess + + eq_(sess.query(User).order_by(User.id).all(), [jack,jane]) + + def test_delete_with_bindparams(self): + User = self.classes.User + + sess = Session() + + john,jack,jill,jane = sess.query(User).order_by(User.id).all() + sess.query(User).filter('name = :name').params(name='john').delete('fetch') + assert john not in sess + + eq_(sess.query(User).order_by(User.id).all(), [jack,jill,jane]) + + def test_delete_rollback(self): + User = self.classes.User + + sess = Session() + john,jack,jill,jane = sess.query(User).order_by(User.id).all() + sess.query(User).filter(or_(User.name == 'john', User.name == 'jill')).\ + delete(synchronize_session='evaluate') + assert john not in sess and jill not in sess + sess.rollback() + assert john in sess and jill in sess + + def test_delete_rollback_with_fetch(self): + User = self.classes.User + + sess = Session() + john,jack,jill,jane = sess.query(User).order_by(User.id).all() + sess.query(User).filter(or_(User.name == 'john', User.name == 'jill')).\ + delete(synchronize_session='fetch') + assert john not in sess and jill not in sess + sess.rollback() + assert john in sess and jill in sess + + def test_delete_without_session_sync(self): + User = self.classes.User + + sess = Session() + + john,jack,jill,jane = sess.query(User).order_by(User.id).all() + sess.query(User).filter(or_(User.name == 'john', User.name == 'jill')).\ + delete(synchronize_session=False) + + assert john in sess and jill in sess + + eq_(sess.query(User).order_by(User.id).all(), [jack,jane]) + + def test_delete_with_fetch_strategy(self): + User = self.classes.User + + sess = Session() + + john,jack,jill,jane = sess.query(User).order_by(User.id).all() + sess.query(User).filter(or_(User.name == 'john', User.name == 'jill')).\ + delete(synchronize_session='fetch') + + assert john not in sess and jill not in sess + + eq_(sess.query(User).order_by(User.id).all(), [jack,jane]) + + @testing.fails_on('mysql', 'FIXME: unknown') + def test_delete_invalid_evaluation(self): + User = self.classes.User + + sess = Session() + + john,jack,jill,jane = sess.query(User).order_by(User.id).all() + + assert_raises(exc.InvalidRequestError, + sess.query(User). + filter(User.name == select([func.max(User.name)])).delete, + synchronize_session='evaluate' + ) + + sess.query(User).filter(User.name == select([func.max(User.name)])).\ + delete(synchronize_session='fetch') + + assert john not in sess + + eq_(sess.query(User).order_by(User.id).all(), [jack,jill,jane]) + + def test_update(self): + User, users = self.classes.User, self.tables.users + + sess = Session() + + john,jack,jill,jane = sess.query(User).order_by(User.id).all() + sess.query(User).filter(User.age > 29).\ + update({'age': User.age - 10}, synchronize_session='evaluate') + + eq_([john.age, jack.age, jill.age, jane.age], [25,37,29,27]) + eq_(sess.query(User.age).order_by(User.id).all(), zip([25,37,29,27])) + + sess.query(User).filter(User.age > 29).\ + update({User.age: User.age - 10}, synchronize_session='evaluate') + eq_([john.age, jack.age, jill.age, jane.age], [25,27,29,27]) + eq_(sess.query(User.age).order_by(User.id).all(), zip([25,27,29,27])) + + sess.query(User).filter(User.age > 27).\ + update({users.c.age: User.age - 10}, synchronize_session='evaluate') + eq_([john.age, jack.age, jill.age, jane.age], [25,27,19,27]) + eq_(sess.query(User.age).order_by(User.id).all(), zip([25,27,19,27])) + + sess.query(User).filter(User.age == 25).\ + update({User.age: User.age - 10}, synchronize_session='fetch') + eq_([john.age, jack.age, jill.age, jane.age], [15,27,19,27]) + eq_(sess.query(User.age).order_by(User.id).all(), zip([15,27,19,27])) + + def test_update_with_bindparams(self): + User = self.classes.User + + sess = Session() + + john,jack,jill,jane = sess.query(User).order_by(User.id).all() + + sess.query(User).filter('age > :x').params(x=29).update({'age': User.age - 10}, synchronize_session='fetch') + + eq_([john.age, jack.age, jill.age, jane.age], [25,37,29,27]) + eq_(sess.query(User.age).order_by(User.id).all(), zip([25,37,29,27])) + + def test_update_changes_resets_dirty(self): + User = self.classes.User + + sess = Session(autoflush=False) + + john,jack,jill,jane = sess.query(User).order_by(User.id).all() + + john.age = 50 + jack.age = 37 + + # autoflush is false. therefore our '50' and '37' are getting + # blown away by this operation. + + sess.query(User).filter(User.age > 29).\ + update({'age': User.age - 10}, synchronize_session='evaluate') + + for x in (john, jack, jill, jane): + assert not sess.is_modified(x) + + eq_([john.age, jack.age, jill.age, jane.age], [25,37,29,27]) + + john.age = 25 + assert john in sess.dirty + assert jack in sess.dirty + assert jill not in sess.dirty + assert not sess.is_modified(john) + assert not sess.is_modified(jack) + + def test_update_changes_with_autoflush(self): + User = self.classes.User + + sess = Session() + + john,jack,jill,jane = sess.query(User).order_by(User.id).all() + + john.age = 50 + jack.age = 37 + + sess.query(User).filter(User.age > 29).\ + update({'age': User.age - 10}, synchronize_session='evaluate') + + for x in (john, jack, jill, jane): + assert not sess.is_modified(x) + + eq_([john.age, jack.age, jill.age, jane.age], [40, 27, 29, 27]) + + john.age = 25 + assert john in sess.dirty + assert jack not in sess.dirty + assert jill not in sess.dirty + assert sess.is_modified(john) + assert not sess.is_modified(jack) + + + + def test_update_with_expire_strategy(self): + User = self.classes.User + + sess = Session() + + john,jack,jill,jane = sess.query(User).order_by(User.id).all() + sess.query(User).filter(User.age > 29).\ + update({'age': User.age - 10}, synchronize_session='fetch') + + eq_([john.age, jack.age, jill.age, jane.age], [25,37,29,27]) + eq_(sess.query(User.age).order_by(User.id).all(), zip([25,37,29,27])) + + @testing.fails_if(lambda: not testing.db.dialect.supports_sane_rowcount) + def test_update_returns_rowcount(self): + User = self.classes.User + + sess = Session() + + rowcount = sess.query(User).filter(User.age > 29).update({'age': User.age + 0}) + eq_(rowcount, 2) + + rowcount = sess.query(User).filter(User.age > 29).update({'age': User.age - 10}) + eq_(rowcount, 2) + + @testing.fails_if(lambda: not testing.db.dialect.supports_sane_rowcount) + def test_delete_returns_rowcount(self): + User = self.classes.User + + sess = Session() + + rowcount = sess.query(User).filter(User.age > 26).delete(synchronize_session=False) + eq_(rowcount, 3) + + def test_update_with_eager_relationships(self): + Document = self.classes.Document + + sess = Session() + + foo,bar,baz = sess.query(Document).order_by(Document.id).all() + sess.query(Document).filter(Document.user_id == 1).\ + update({'title': Document.title+Document.title}, synchronize_session='fetch') + + eq_([foo.title, bar.title, baz.title], ['foofoo','barbar', 'baz']) + eq_(sess.query(Document.title).order_by(Document.id).all(), + zip(['foofoo','barbar', 'baz'])) + + def test_update_with_explicit_joinedload(self): + User = self.classes.User + + sess = Session() + + john,jack,jill,jane = sess.query(User).order_by(User.id).all() + sess.query(User).options(joinedload(User.documents)).filter(User.age > 29).\ + update({'age': User.age - 10}, synchronize_session='fetch') + + eq_([john.age, jack.age, jill.age, jane.age], [25,37,29,27]) + eq_(sess.query(User.age).order_by(User.id).all(), zip([25,37,29,27])) + + def test_delete_with_eager_relationships(self): + Document = self.classes.Document + + sess = Session() + + sess.query(Document).filter(Document.user_id == 1).\ + delete(synchronize_session=False) + + eq_(sess.query(Document.title).all(), zip(['baz'])) + + def test_update_all(self): + User = self.classes.User + + sess = Session() + + john,jack,jill,jane = sess.query(User).order_by(User.id).all() + sess.query(User).update({'age': 42}, synchronize_session='evaluate') + + eq_([john.age, jack.age, jill.age, jane.age], [42,42,42,42]) + eq_(sess.query(User.age).order_by(User.id).all(), zip([42,42,42,42])) + + def test_delete_all(self): + User = self.classes.User + + sess = Session() + + john,jack,jill,jane = sess.query(User).order_by(User.id).all() + sess.query(User).delete(synchronize_session='evaluate') + + assert not (john in sess or jack in sess or jill in sess or jane in sess) + eq_(sess.query(User).count(), 0) + + def test_autoflush_before_evaluate_update(self): + User = self.classes.User + + sess = Session() + john = sess.query(User).filter_by(name='john').one() + john.name = 'j2' + + sess.query(User).filter_by(name='j2').\ + update({'age':42}, + synchronize_session='evaluate') + eq_(john.age, 42) + + def test_autoflush_before_fetch_update(self): + User = self.classes.User + + sess = Session() + john = sess.query(User).filter_by(name='john').one() + john.name = 'j2' + + sess.query(User).filter_by(name='j2').\ + update({'age':42}, + synchronize_session='fetch') + eq_(john.age, 42) + + def test_autoflush_before_evaluate_delete(self): + User = self.classes.User + + sess = Session() + john = sess.query(User).filter_by(name='john').one() + john.name = 'j2' + + sess.query(User).filter_by(name='j2').\ + delete( + synchronize_session='evaluate') + assert john not in sess + + def test_autoflush_before_fetch_delete(self): + User = self.classes.User + + sess = Session() + john = sess.query(User).filter_by(name='john').one() + john.name = 'j2' + + sess.query(User).filter_by(name='j2').\ + delete( + synchronize_session='fetch') + assert john not in sess + + def test_evaluate_before_update(self): + User = self.classes.User + + sess = Session() + john = sess.query(User).filter_by(name='john').one() + sess.expire(john, ['age']) + + # eval must be before the update. otherwise + # we eval john, age has been expired and doesn't + # match the new value coming in + sess.query(User).filter_by(name='john').filter_by(age=25).\ + update({'name':'j2', 'age':40}, + synchronize_session='evaluate') + eq_(john.name, 'j2') + eq_(john.age, 40) + + def test_fetch_before_update(self): + User = self.classes.User + + sess = Session() + john = sess.query(User).filter_by(name='john').one() + sess.expire(john, ['age']) + + sess.query(User).filter_by(name='john').filter_by(age=25).\ + update({'name':'j2', 'age':40}, + synchronize_session='fetch') + eq_(john.name, 'j2') + eq_(john.age, 40) + + def test_evaluate_before_delete(self): + User = self.classes.User + + sess = Session() + john = sess.query(User).filter_by(name='john').one() + sess.expire(john, ['age']) + + sess.query(User).filter_by(name='john').\ + filter_by(age=25).\ + delete( + synchronize_session='evaluate') + assert john not in sess + + def test_fetch_before_delete(self): + User = self.classes.User + + sess = Session() + john = sess.query(User).filter_by(name='john').one() + sess.expire(john, ['age']) + + sess.query(User).filter_by(name='john').\ + filter_by(age=25).\ + delete( + synchronize_session='fetch') + assert john not in sess + +class ExpressionUpdateTest(fixtures.MappedTest): + @classmethod + def define_tables(cls, metadata): + data = Table('data', metadata, + Column('id', Integer, primary_key=True, + test_needs_autoincrement=True), + Column('counter', Integer, nullable=False, default=0) + ) + + @classmethod + def setup_classes(cls): + class Data(cls.Comparable): + pass + + @classmethod + def setup_mappers(cls): + data = cls.tables.data + mapper(cls.classes.Data, data, properties={'cnt':data.c.counter}) + + @testing.provide_metadata + def test_update_attr_names(self): + Data = self.classes.Data + + d1 = Data() + sess = Session() + sess.add(d1) + sess.commit() + eq_(d1.cnt, 0) + + sess.query(Data).update({Data.cnt:Data.cnt + 1}) + sess.flush() + + eq_(d1.cnt, 1) + + sess.query(Data).update({Data.cnt:Data.cnt + 1}, 'fetch') + sess.flush() + + eq_(d1.cnt, 2) + sess.close() + +