From a0c4060adfc2b6fc5c258ec29a878ea7fed98a52 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 25 Jan 2009 00:48:15 +0000 Subject: [PATCH] - the 'expire' option on query.update() has been renamed to 'fetch', thus matching that of query.delete() - query.update() and query.delete() both default to 'evaluate' for the synchronize strategy. - the 'synchronize' strategy for update() and delete() raises an error on failure. There is no implicit fallback onto "fetch". Failure of evaluation is based on the structure of criteria, so success/failure is deterministic based on code structure. --- 06CHANGES | 15 ++++++++++++ lib/sqlalchemy/orm/query.py | 46 ++++++++++++++++++------------------- test/orm/query.py | 15 ++++++++---- 3 files changed, 48 insertions(+), 28 deletions(-) create mode 100644 06CHANGES diff --git a/06CHANGES b/06CHANGES new file mode 100644 index 0000000000..17a0a50f23 --- /dev/null +++ b/06CHANGES @@ -0,0 +1,15 @@ +- orm + - the 'expire' option on query.update() has been renamed to 'fetch', thus matching + that of query.delete() + - query.update() and query.delete() both default to 'evaluate' for the synchronize + strategy. + - the 'synchronize' strategy for update() and delete() raises an error on failure. + There is no implicit fallback onto "fetch". Failure of evaluation is based + on the structure of criteria, so success/failure is deterministic based on + code structure. + +- dialect refactor + +- new dialects + - pg8000 + - pyodbc+mysql \ No newline at end of file diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 6a26d30b44..c5d0f72695 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -1495,7 +1495,7 @@ class Query(object): self.session._autoflush() return self.session.scalar(s, params=self._params, mapper=self._mapper_zero()) - def delete(self, synchronize_session='fetch'): + def delete(self, synchronize_session='evaluate'): """Perform a bulk delete query. Deletes rows matched by this query from the database. @@ -1505,15 +1505,15 @@ class Query(object): False don't synchronize the session. This option is the most efficient and is reliable - once the session is expired, which typically occurs after a commit(). Before - the expiration, objects may still remain in the session which were in fact deleted - which can lead to confusing results if they are accessed via get() or already - loaded collections. + once the session is expired, which typically occurs after a commit(), or explicitly + using expire_all(). Before the expiration, objects may still remain in the session + which were in fact deleted which can lead to confusing results if they are accessed + via get() or already loaded collections. 'fetch' performs a select query before the delete to find objects that are matched by the delete query and need to be removed from the session. Matched objects - are removed from the session. 'fetch' is the default strategy. + are removed from the session. 'evaluate' experimental feature. Tries to evaluate the querys criteria in Python @@ -1554,7 +1554,8 @@ class Query(object): evaluator_compiler = evaluator.EvaluatorCompiler() eval_condition = evaluator_compiler.process(self.whereclause) except evaluator.UnevaluatableError: - synchronize_session = 'fetch' + raise sa_exc.InvalidRequestError("Could not evaluate current criteria in Python. " + "Specify 'fetch' or False for the synchronize_session parameter.") delete_stmt = sql.delete(primary_table, context.whereclause) @@ -1587,7 +1588,7 @@ class Query(object): return result.rowcount - def update(self, values, synchronize_session='expire'): + def update(self, values, synchronize_session='evaluate'): """Perform a bulk update query. Updates rows matched by this query in the database. @@ -1599,18 +1600,19 @@ class Query(object): attributes on objects in the session. Valid values are: False - don't synchronize the session. Use this when you don't need to use the - session after the update or you can be sure that none of the matched objects - are in the session. - - 'expire' + don't synchronize the session. This option is the most efficient and is reliable + once the session is expired, which typically occurs after a commit(), or explicitly + using expire_all(). Before the expiration, updated objects may still remain in the session + with stale values on their attributes, which can lead to confusing results. + + 'fetch' performs a select query before the update to find objects that are matched by the update query. The updated attributes are expired on matched objects. 'evaluate' - experimental feature. Tries to evaluate the querys criteria in Python + Tries to evaluate the Query's criteria in Python straight on the objects in the session. If evaluation of the criteria isn't - implemented, the 'expire' strategy will be used as a fallback. + implemented, an exception is raised. The expression evaluator currently doesn't account for differing string collations between the database and Python. @@ -1619,9 +1621,6 @@ class Query(object): The method does *not* offer in-Python cascading of relations - it is assumed that ON UPDATE CASCADE is configured for any foreign key references which require it. - The Session needs to be expired (occurs automatically after commit(), or call expire_all()) - in order for the state of dependent objects subject foreign key cascade to be - correctly represented. Also, the ``before_update()`` and ``after_update()`` :class:`~sqlalchemy.orm.interfaces.MapperExtension` methods are not called from this method. For an update hook here, use the @@ -1633,8 +1632,8 @@ class Query(object): #TODO: updates of manytoone relations need to be converted to fk assignments #TODO: cascades need handling. - if synchronize_session not in [False, 'evaluate', 'expire']: - raise sa_exc.ArgumentError("Valid strategies for session synchronization are False, 'evaluate' and 'expire'") + if synchronize_session not in [False, 'evaluate', 'fetch']: + raise sa_exc.ArgumentError("Valid strategies for session synchronization are False, 'evaluate' and 'fetch'") context = self._compile_context() if len(context.statement.froms) != 1 or not isinstance(context.statement.froms[0], schema.Table): @@ -1653,11 +1652,12 @@ class Query(object): key = expression._column_as_key(key) value_evaluators[key] = evaluator_compiler.process(expression._literal_as_binds(value)) except evaluator.UnevaluatableError: - synchronize_session = 'expire' + raise sa_exc.InvalidRequestError("Could not evaluate current criteria in Python. " + "Specify 'fetch' or False for the synchronize_session parameter.") update_stmt = sql.update(primary_table, context.whereclause, values) - if synchronize_session == 'expire': + if synchronize_session == 'fetch': select_stmt = context.statement.with_only_columns(primary_table.primary_key) matched_rows = session.execute(select_stmt, params=self._params).fetchall() @@ -1684,7 +1684,7 @@ class Query(object): # expire attributes with pending changes (there was no autoflush, so they are overwritten) state.expire_attributes(set(evaluated_keys).difference(to_evaluate)) - elif synchronize_session == 'expire': + elif synchronize_session == 'fetch': target_mapper = self._mapper_zero() for primary_key in matched_rows: diff --git a/test/orm/query.py b/test/orm/query.py index c0c966855d..6f0b69f17a 100644 --- a/test/orm/query.py +++ b/test/orm/query.py @@ -2761,7 +2761,7 @@ class UpdateDeleteTest(_base.MappedTest): 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() + 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]) @@ -2808,11 +2808,16 @@ class UpdateDeleteTest(_base.MappedTest): @testing.fails_on('mysql', 'FIXME: unknown') @testing.resolve_artifact_names - def test_delete_fallback(self): + def test_delete_invalid_evaluation(self): 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.name == select([func.max(User.name)])).delete(synchronize_session='evaluate') + + self.assertRaises(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 @@ -2843,7 +2848,7 @@ class UpdateDeleteTest(_base.MappedTest): 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='evaluate') + 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])) @@ -2903,7 +2908,7 @@ class UpdateDeleteTest(_base.MappedTest): 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='expire') + 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])) -- 2.47.3