From 0f9560855e5ed203b8c911c23237826e28a62a38 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Fri, 21 Mar 2014 14:21:43 +0100 Subject: [PATCH] Removed legacy transaction management per the deprecation timeline. --- django/contrib/contenttypes/fields.py | 2 +- .../management/commands/createcachetable.py | 2 +- django/core/management/commands/flush.py | 2 +- django/core/management/commands/loaddata.py | 2 +- django/core/management/commands/migrate.py | 8 +- django/db/__init__.py | 21 +- django/db/backends/__init__.py | 149 +------ django/db/backends/dummy/base.py | 2 - .../db/backends/postgresql_psycopg2/base.py | 3 - django/db/backends/utils.py | 3 - django/db/models/base.py | 4 +- django/db/models/deletion.py | 2 +- django/db/models/fields/related.py | 5 +- django/db/models/query.py | 6 +- django/db/transaction.py | 264 +----------- django/middleware/transaction.py | 58 --- django/test/testcases.py | 11 +- tests/backends/tests.py | 2 +- tests/basic/tests.py | 1 - tests/delete_regress/tests.py | 28 +- tests/middleware/models.py | 13 - tests/middleware/tests.py | 69 +-- tests/requests/tests.py | 62 +-- tests/select_for_update/tests.py | 47 +-- tests/serializers/tests.py | 17 +- tests/transactions/tests.py | 340 +-------------- tests/transactions_regress/__init__.py | 0 tests/transactions_regress/models.py | 17 - tests/transactions_regress/tests.py | 392 ------------------ 29 files changed, 78 insertions(+), 1454 deletions(-) delete mode 100644 django/middleware/transaction.py delete mode 100644 tests/middleware/models.py delete mode 100644 tests/transactions_regress/__init__.py delete mode 100644 tests/transactions_regress/models.py delete mode 100644 tests/transactions_regress/tests.py diff --git a/django/contrib/contenttypes/fields.py b/django/contrib/contenttypes/fields.py index afb6105a22..b551d66b13 100644 --- a/django/contrib/contenttypes/fields.py +++ b/django/contrib/contenttypes/fields.py @@ -500,7 +500,7 @@ def create_generic_related_manager(superclass): if bulk: queryset.delete() else: - with transaction.commit_on_success_unless_managed(using=db, savepoint=False): + with transaction.atomic(using=db, savepoint=False): for obj in queryset: obj.delete() _clear.alters_data = True diff --git a/django/core/management/commands/createcachetable.py b/django/core/management/commands/createcachetable.py index 909a5d08c8..d170ee64c9 100644 --- a/django/core/management/commands/createcachetable.py +++ b/django/core/management/commands/createcachetable.py @@ -71,7 +71,7 @@ class Command(BaseCommand): for i, line in enumerate(table_output): full_statement.append(' %s%s' % (line, ',' if i < len(table_output) - 1 else '')) full_statement.append(');') - with transaction.commit_on_success_unless_managed(): + with transaction.atomic(): with connection.cursor() as curs: try: curs.execute("\n".join(full_statement)) diff --git a/django/core/management/commands/flush.py b/django/core/management/commands/flush.py index d99deb951e..0d13b35052 100644 --- a/django/core/management/commands/flush.py +++ b/django/core/management/commands/flush.py @@ -63,7 +63,7 @@ Are you sure you want to do this? if confirm == 'yes': try: - with transaction.commit_on_success_unless_managed(): + with transaction.atomic(): with connection.cursor() as cursor: for sql in sql_list: cursor.execute(sql) diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index 7a008bca08..44583bd41e 100644 --- a/django/core/management/commands/loaddata.py +++ b/django/core/management/commands/loaddata.py @@ -56,7 +56,7 @@ class Command(BaseCommand): self.verbosity = int(options.get('verbosity')) - with transaction.commit_on_success_unless_managed(using=self.using): + with transaction.atomic(using=self.using): self.loaddata(fixture_labels) # Close the DB connection -- unless we're still in a transaction. This diff --git a/django/core/management/commands/migrate.py b/django/core/management/commands/migrate.py index 50da31e11e..1ca4ec7957 100644 --- a/django/core/management/commands/migrate.py +++ b/django/core/management/commands/migrate.py @@ -223,10 +223,6 @@ class Command(BaseCommand): for statement in sql: cursor.execute(statement) tables.append(connection.introspection.table_name_converter(model._meta.db_table)) - - # We force a commit here, as that was the previous behavior. - # If you can prove we don't need this, remove it. - transaction.set_dirty(using=connection.alias) finally: cursor.close() @@ -245,7 +241,7 @@ class Command(BaseCommand): if self.verbosity >= 2: self.stdout.write(" Installing custom SQL for %s.%s model\n" % (app_name, model._meta.object_name)) try: - with transaction.commit_on_success_unless_managed(using=connection.alias): + with transaction.atomic(using=connection.alias): for sql in custom_sql: cursor.execute(sql) except Exception as e: @@ -268,7 +264,7 @@ class Command(BaseCommand): if self.verbosity >= 2: self.stdout.write(" Installing index for %s.%s model\n" % (app_name, model._meta.object_name)) try: - with transaction.commit_on_success_unless_managed(using=connection.alias): + with transaction.atomic(using=connection.alias): for sql in index_sql: cursor.execute(sql) except Exception as e: diff --git a/django/db/__init__.py b/django/db/__init__.py index 928b652fcf..bbda6a1ee3 100644 --- a/django/db/__init__.py +++ b/django/db/__init__.py @@ -77,20 +77,6 @@ class DefaultBackendProxy(object): backend = DefaultBackendProxy() -def close_connection(**kwargs): - warnings.warn( - "close_connection is superseded by close_old_connections.", - RemovedInDjango18Warning, stacklevel=2) - # Avoid circular imports - from django.db import transaction - for conn in connections: - # If an error happens here the connection will be left in broken - # state. Once a good db connection is again available, the - # connection state will be cleaned up. - transaction.abort(conn) - connections[conn].close() - - # Register an event to reset saved queries when a Django request is started. def reset_queries(**kwargs): for conn in connections.all(): @@ -99,14 +85,9 @@ signals.request_started.connect(reset_queries) # Register an event to reset transaction state and close connections past -# their lifetime. NB: abort() doesn't do anything outside of a transaction. +# their lifetime. def close_old_connections(**kwargs): for conn in connections.all(): - # Remove this when the legacy transaction management goes away. - try: - conn.abort() - except DatabaseError: - pass conn.close_if_unusable_or_obsolete() signals.request_started.connect(close_old_connections) signals.request_finished.connect(close_old_connections) diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index b274065a21..f70d7759e0 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -30,28 +30,25 @@ class BaseDatabaseWrapper(object): def __init__(self, settings_dict, alias=DEFAULT_DB_ALIAS, allow_thread_sharing=False): + # Connection related attributes. + self.connection = None + self.queries = [] # `settings_dict` should be a dictionary containing keys such as # NAME, USER, etc. It's called `settings_dict` instead of `settings` # to disambiguate it from Django settings modules. - self.connection = None - self.queries = [] self.settings_dict = settings_dict self.alias = alias self.use_debug_cursor = None - # Savepoint management related attributes - self.savepoint_state = 0 - - # Transaction management related attributes + # Transaction related attributes. + # Tracks if the connection is in autocommit mode. Per PEP 249, by + # default, it isn't. self.autocommit = False - self.transaction_state = [] - # Tracks if the connection is believed to be in transaction. This is - # set somewhat aggressively, as the DBAPI doesn't make it easy to - # deduce if the connection is in transaction or not. - self._dirty = False # Tracks if the connection is in a transaction managed by 'atomic'. self.in_atomic_block = False - # List of savepoints created by 'atomic' + # Increment to generate unique savepoint ids. + self.savepoint_state = 0 + # List of savepoints created by 'atomic'. self.savepoint_ids = [] # Tracks if the outermost 'atomic' block should commit on exit, # ie. if autocommit was active on entry. @@ -60,11 +57,11 @@ class BaseDatabaseWrapper(object): # available savepoint because of an exception in an inner block. self.needs_rollback = False - # Connection termination related attributes + # Connection termination related attributes. self.close_at = None self.errors_occurred = False - # Thread-safety related attributes + # Thread-safety related attributes. self.allow_thread_sharing = allow_thread_sharing self._thread_ident = thread.get_ident() @@ -166,7 +163,6 @@ class BaseDatabaseWrapper(object): self.validate_thread_sharing() self.validate_no_atomic_block() self._commit() - self.set_clean() def rollback(self): """ @@ -175,7 +171,6 @@ class BaseDatabaseWrapper(object): self.validate_thread_sharing() self.validate_no_atomic_block() self._rollback() - self.set_clean() def close(self): """ @@ -189,7 +184,6 @@ class BaseDatabaseWrapper(object): self._close() finally: self.connection = None - self.set_clean() ##### Backend-specific savepoint management methods ##### @@ -267,59 +261,6 @@ class BaseDatabaseWrapper(object): ##### Generic transaction management methods ##### - def enter_transaction_management(self, managed=True, forced=False): - """ - Enters transaction management for a running thread. It must be balanced with - the appropriate leave_transaction_management call, since the actual state is - managed as a stack. - - The state and dirty flag are carried over from the surrounding block or - from the settings, if there is no surrounding block (dirty is always false - when no current block is running). - - If you switch off transaction management and there is a pending - commit/rollback, the data will be committed, unless "forced" is True. - """ - self.validate_no_atomic_block() - - self.transaction_state.append(managed) - - if not managed and self.is_dirty() and not forced: - self.commit() - self.set_clean() - - if managed == self.get_autocommit(): - self.set_autocommit(not managed) - - def leave_transaction_management(self): - """ - Leaves transaction management for a running thread. A dirty flag is carried - over to the surrounding block, as a commit will commit all changes, even - those from outside. (Commits are on connection level.) - """ - self.validate_no_atomic_block() - - if self.transaction_state: - del self.transaction_state[-1] - else: - raise TransactionManagementError( - "This code isn't under transaction management") - - if self.transaction_state: - managed = self.transaction_state[-1] - else: - managed = not self.settings_dict['AUTOCOMMIT'] - - if self._dirty: - self.rollback() - if managed == self.get_autocommit(): - self.set_autocommit(not managed) - raise TransactionManagementError( - "Transaction managed block ended with pending COMMIT/ROLLBACK") - - if managed == self.get_autocommit(): - self.set_autocommit(not managed) - def get_autocommit(self): """ Check the autocommit state. @@ -368,41 +309,6 @@ class BaseDatabaseWrapper(object): "An error occurred in the current transaction. You can't " "execute queries until the end of the 'atomic' block.") - def abort(self): - """ - Roll back any ongoing transaction and clean the transaction state - stack. - """ - if self._dirty: - self.rollback() - while self.transaction_state: - self.leave_transaction_management() - - def is_dirty(self): - """ - Returns True if the current transaction requires a commit for changes to - happen. - """ - return self._dirty - - def set_dirty(self): - """ - Sets a dirty flag for the current thread and code streak. This can be used - to decide in a managed block of code to decide whether there are open - changes waiting for commit. - """ - if not self.get_autocommit(): - self._dirty = True - - def set_clean(self): - """ - Resets a dirty flag for the current thread and code streak. This can be used - to decide in a managed block of code to decide whether a commit or rollback - should happen. - """ - self._dirty = False - self.clean_savepoints() - ##### Foreign key constraints checks handling ##### @contextmanager @@ -576,10 +482,6 @@ class BaseDatabaseFeatures(object): # at the end of each save operation? supports_forward_references = True - # Does a dirty transaction need to be rolled back - # before the cursor can be used again? - requires_rollback_on_dirty_transaction = False - # Does the backend allow very long model names without error? supports_long_model_names = True @@ -682,28 +584,21 @@ class BaseDatabaseFeatures(object): @cached_property def supports_transactions(self): - "Confirm support for transactions" - try: - # Make sure to run inside a managed transaction block, - # otherwise autocommit will cause the confimation to - # fail. - self.connection.enter_transaction_management() - with self.connection.cursor() as cursor: - cursor.execute('CREATE TABLE ROLLBACK_TEST (X INT)') - self.connection.commit() - cursor.execute('INSERT INTO ROLLBACK_TEST (X) VALUES (8)') - self.connection.rollback() - cursor.execute('SELECT COUNT(X) FROM ROLLBACK_TEST') - count, = cursor.fetchone() - cursor.execute('DROP TABLE ROLLBACK_TEST') - self.connection.commit() - finally: - self.connection.leave_transaction_management() + """Confirm support for transactions.""" + with self.connection.cursor() as cursor: + cursor.execute('CREATE TABLE ROLLBACK_TEST (X INT)') + self.connection.set_autocommit(False) + cursor.execute('INSERT INTO ROLLBACK_TEST (X) VALUES (8)') + self.connection.rollback() + self.connection.set_autocommit(True) + cursor.execute('SELECT COUNT(X) FROM ROLLBACK_TEST') + count, = cursor.fetchone() + cursor.execute('DROP TABLE ROLLBACK_TEST') return count == 0 @cached_property def supports_stddev(self): - "Confirm support for STDDEV and related stats functions" + """Confirm support for STDDEV and related stats functions.""" class StdDevPop(object): sql_function = 'STDDEV_POP' diff --git a/django/db/backends/dummy/base.py b/django/db/backends/dummy/base.py index 3e6d3e4c5a..92075ac85e 100644 --- a/django/db/backends/dummy/base.py +++ b/django/db/backends/dummy/base.py @@ -67,8 +67,6 @@ class DatabaseWrapper(BaseDatabaseWrapper): _savepoint_commit = complain _savepoint_rollback = ignore _set_autocommit = complain - set_dirty = complain - set_clean = complain def __init__(self, *args, **kwargs): super(DatabaseWrapper, self).__init__(*args, **kwargs) diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index e89a4e604a..dbba3d6d7d 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -48,7 +48,6 @@ def utc_tzinfo_factory(offset): class DatabaseFeatures(BaseDatabaseFeatures): needs_datetime_string_cast = False can_return_id_from_insert = True - requires_rollback_on_dirty_transaction = True has_real_datatype = True can_defer_constraint_checks = True has_select_for_update = True @@ -181,8 +180,6 @@ class DatabaseWrapper(BaseDatabaseWrapper): exc_info=sys.exc_info() ) raise - finally: - self.set_clean() def _set_isolation_level(self, isolation_level): assert isolation_level in range(1, 5) # Use set_autocommit for level = 0 diff --git a/django/db/backends/utils.py b/django/db/backends/utils.py index 8bed6415c3..c9f56accc2 100644 --- a/django/db/backends/utils.py +++ b/django/db/backends/utils.py @@ -44,7 +44,6 @@ class CursorWrapper(object): def callproc(self, procname, params=None): self.db.validate_no_broken_transaction() - self.db.set_dirty() with self.db.wrap_database_errors: if params is None: return self.cursor.callproc(procname) @@ -53,7 +52,6 @@ class CursorWrapper(object): def execute(self, sql, params=None): self.db.validate_no_broken_transaction() - self.db.set_dirty() with self.db.wrap_database_errors: if params is None: return self.cursor.execute(sql) @@ -62,7 +60,6 @@ class CursorWrapper(object): def executemany(self, sql, param_list): self.db.validate_no_broken_transaction() - self.db.set_dirty() with self.db.wrap_database_errors: return self.cursor.executemany(sql, param_list) diff --git a/django/db/models/base.py b/django/db/models/base.py index ba89e6e037..e0bb1690ec 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -625,7 +625,7 @@ class Model(six.with_metaclass(ModelBase)): if not meta.auto_created: signals.pre_save.send(sender=origin, instance=self, raw=raw, using=using, update_fields=update_fields) - with transaction.commit_on_success_unless_managed(using=using, savepoint=False): + with transaction.atomic(using=using, savepoint=False): if not raw: self._save_parents(cls, using, update_fields) updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields) @@ -1417,7 +1417,7 @@ def method_set_order(ordered_obj, self, id_list, using=None): order_name = ordered_obj._meta.order_with_respect_to.name # FIXME: It would be nice if there was an "update many" version of update # for situations like this. - with transaction.commit_on_success_unless_managed(using=using): + with transaction.atomic(using=using, savepoint=False): for i, j in enumerate(id_list): ordered_obj.objects.filter(**{'pk': j, order_name: rel_val}).update(_order=i) diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py index 7d64680b37..bed3913fb0 100644 --- a/django/db/models/deletion.py +++ b/django/db/models/deletion.py @@ -249,7 +249,7 @@ class Collector(object): # end of a transaction. self.sort() - with transaction.commit_on_success_unless_managed(using=self.using): + with transaction.atomic(using=self.using, savepoint=False): # send pre_delete signals for model, obj in self.instances_with_model(): if not model._meta.auto_created: diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 148b979ad3..ef48ff538b 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -686,8 +686,7 @@ def create_foreign_related_manager(superclass, rel_field, rel_model): def add(self, *objs): objs = list(objs) db = router.db_for_write(self.model, instance=self.instance) - with transaction.commit_on_success_unless_managed( - using=db, savepoint=False): + with transaction.atomic(using=db, savepoint=False): for obj in objs: if not isinstance(obj, self.model): raise TypeError("'%s' instance expected, got %r" % @@ -738,7 +737,7 @@ def create_foreign_related_manager(superclass, rel_field, rel_model): if bulk: queryset.update(**{rel_field.name: None}) else: - with transaction.commit_on_success_unless_managed(using=db, savepoint=False): + with transaction.atomic(using=db, savepoint=False): for obj in queryset: setattr(obj, rel_field.name, None) obj.save(update_fields=[rel_field.name]) diff --git a/django/db/models/query.py b/django/db/models/query.py index f69ac27b65..a555e98c1a 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -391,7 +391,7 @@ class QuerySet(object): self._for_write = True connection = connections[self.db] fields = self.model._meta.local_concrete_fields - with transaction.commit_on_success_unless_managed(using=self.db): + with transaction.atomic(using=self.db, savepoint=False): if (connection.features.can_combine_inserts_with_and_without_auto_increment_pk and self.model._meta.has_auto_field): self._batched_insert(objs, fields, batch_size) @@ -437,7 +437,7 @@ class QuerySet(object): for k, v in six.iteritems(defaults): setattr(obj, k, v) - with transaction.atomic(using=self.db): + with transaction.atomic(using=self.db, savepoint=False): obj.save(using=self.db) return obj, False @@ -574,7 +574,7 @@ class QuerySet(object): self._for_write = True query = self.query.clone(sql.UpdateQuery) query.add_update_values(kwargs) - with transaction.commit_on_success_unless_managed(using=self.db): + with transaction.atomic(using=self.db, savepoint=False): rows = query.get_compiler(self.db).execute_sql(CURSOR) self._result_cache = None return rows diff --git a/django/db/transaction.py b/django/db/transaction.py index 61cb39791e..e78ed9b421 100644 --- a/django/db/transaction.py +++ b/django/db/transaction.py @@ -1,26 +1,9 @@ -""" -This module implements a transaction manager that can be used to define -transaction handling in a request or view function. It is used by transaction -control middleware and decorators. - -The transaction manager can be in managed or in auto state. Auto state means the -system is using a commit-on-save strategy (actually it's more like -commit-on-change). As soon as the .save() or .delete() (or related) methods are -called, a commit is made. - -Managed transactions don't do those commits, but will need some kind of manual -or implicit commits or rollbacks. -""" - -import warnings - from functools import wraps from django.db import ( connections, DEFAULT_DB_ALIAS, DatabaseError, ProgrammingError) from django.utils.decorators import available_attrs -from django.utils.deprecation import RemovedInDjango18Warning class TransactionManagementError(ProgrammingError): @@ -30,109 +13,16 @@ class TransactionManagementError(ProgrammingError): pass -################ -# Private APIs # -################ - def get_connection(using=None): """ Get a database connection by name, or the default database connection - if no name is provided. + if no name is provided. This is a private API. """ if using is None: using = DEFAULT_DB_ALIAS return connections[using] -########################### -# Deprecated private APIs # -########################### - -def abort(using=None): - """ - Roll back any ongoing transactions and clean the transaction management - state of the connection. - - This method is to be used only in cases where using balanced - leave_transaction_management() calls isn't possible. For example after a - request has finished, the transaction state isn't known, yet the connection - must be cleaned up for the next request. - """ - get_connection(using).abort() - - -def enter_transaction_management(managed=True, using=None, forced=False): - """ - Enters transaction management for a running thread. It must be balanced with - the appropriate leave_transaction_management call, since the actual state is - managed as a stack. - - The state and dirty flag are carried over from the surrounding block or - from the settings, if there is no surrounding block (dirty is always false - when no current block is running). - """ - get_connection(using).enter_transaction_management(managed, forced) - - -def leave_transaction_management(using=None): - """ - Leaves transaction management for a running thread. A dirty flag is carried - over to the surrounding block, as a commit will commit all changes, even - those from outside. (Commits are on connection level.) - """ - get_connection(using).leave_transaction_management() - - -def is_dirty(using=None): - """ - Returns True if the current transaction requires a commit for changes to - happen. - """ - return get_connection(using).is_dirty() - - -def set_dirty(using=None): - """ - Sets a dirty flag for the current thread and code streak. This can be used - to decide in a managed block of code to decide whether there are open - changes waiting for commit. - """ - get_connection(using).set_dirty() - - -def set_clean(using=None): - """ - Resets a dirty flag for the current thread and code streak. This can be used - to decide in a managed block of code to decide whether a commit or rollback - should happen. - """ - get_connection(using).set_clean() - - -def is_managed(using=None): - warnings.warn("'is_managed' is deprecated.", - RemovedInDjango18Warning, stacklevel=2) - - -def managed(flag=True, using=None): - warnings.warn("'managed' no longer serves a purpose.", - RemovedInDjango18Warning, stacklevel=2) - - -def commit_unless_managed(using=None): - warnings.warn("'commit_unless_managed' is now a no-op.", - RemovedInDjango18Warning, stacklevel=2) - - -def rollback_unless_managed(using=None): - warnings.warn("'rollback_unless_managed' is now a no-op.", - RemovedInDjango18Warning, stacklevel=2) - - -############### -# Public APIs # -############### - def get_autocommit(using=None): """ Get the autocommit status of the connection. @@ -149,14 +39,14 @@ def set_autocommit(autocommit, using=None): def commit(using=None): """ - Commits a transaction and resets the dirty flag. + Commits a transaction. """ get_connection(using).commit() def rollback(using=None): """ - Rolls back a transaction and resets the dirty flag. + Rolls back a transaction. """ get_connection(using).rollback() @@ -244,6 +134,8 @@ class Atomic(object): `with oa:` multiple times. Since database connections are thread-local, this is thread-safe. + + This is a private API. """ def __init__(self, using, savepoint): @@ -388,149 +280,3 @@ def non_atomic_requests(using=None): if using is None: using = DEFAULT_DB_ALIAS return lambda view: _non_atomic_requests(view, using) - - -############################################ -# Deprecated decorators / context managers # -############################################ - -class Transaction(object): - """ - Acts as either a decorator, or a context manager. If it's a decorator it - takes a function and returns a wrapped function. If it's a contextmanager - it's used with the ``with`` statement. In either event entering/exiting - are called before and after, respectively, the function/block is executed. - - autocommit, commit_on_success, and commit_manually contain the - implementations of entering and exiting. - """ - def __init__(self, entering, exiting, using): - self.entering = entering - self.exiting = exiting - self.using = using - - def __enter__(self): - self.entering(self.using) - - def __exit__(self, exc_type, exc_value, traceback): - self.exiting(exc_type, self.using) - - def __call__(self, func): - @wraps(func) - def inner(*args, **kwargs): - with self: - return func(*args, **kwargs) - return inner - - -def _transaction_func(entering, exiting, using): - """ - Takes 3 things, an entering function (what to do to start this block of - transaction management), an exiting function (what to do to end it, on both - success and failure, and using which can be: None, indicating using is - DEFAULT_DB_ALIAS, a callable, indicating that using is DEFAULT_DB_ALIAS and - to return the function already wrapped. - - Returns either a Transaction objects, which is both a decorator and a - context manager, or a wrapped function, if using is a callable. - """ - # Note that although the first argument is *called* `using`, it - # may actually be a function; @autocommit and @autocommit('foo') - # are both allowed forms. - if using is None: - using = DEFAULT_DB_ALIAS - if callable(using): - return Transaction(entering, exiting, DEFAULT_DB_ALIAS)(using) - return Transaction(entering, exiting, using) - - -def autocommit(using=None): - """ - Decorator that activates commit on save. This is Django's default behavior; - this decorator is useful if you globally activated transaction management in - your settings file and want the default behavior in some view functions. - """ - warnings.warn("autocommit is deprecated in favor of set_autocommit.", - RemovedInDjango18Warning, stacklevel=2) - - def entering(using): - enter_transaction_management(managed=False, using=using) - - def exiting(exc_type, using): - leave_transaction_management(using=using) - - return _transaction_func(entering, exiting, using) - - -def commit_on_success(using=None): - """ - This decorator activates commit on response. This way, if the view function - runs successfully, a commit is made; if the viewfunc produces an exception, - a rollback is made. This is one of the most common ways to do transaction - control in Web apps. - """ - warnings.warn("commit_on_success is deprecated in favor of atomic.", - RemovedInDjango18Warning, stacklevel=2) - - def entering(using): - enter_transaction_management(using=using) - - def exiting(exc_type, using): - try: - if exc_type is not None: - if is_dirty(using=using): - rollback(using=using) - else: - if is_dirty(using=using): - try: - commit(using=using) - except: - rollback(using=using) - raise - finally: - leave_transaction_management(using=using) - - return _transaction_func(entering, exiting, using) - - -def commit_manually(using=None): - """ - Decorator that activates manual transaction control. It just disables - automatic transaction control and doesn't do any commit/rollback of its - own -- it's up to the user to call the commit and rollback functions - themselves. - """ - warnings.warn("commit_manually is deprecated in favor of set_autocommit.", - RemovedInDjango18Warning, stacklevel=2) - - def entering(using): - enter_transaction_management(using=using) - - def exiting(exc_type, using): - leave_transaction_management(using=using) - - return _transaction_func(entering, exiting, using) - - -def commit_on_success_unless_managed(using=None, savepoint=False): - """ - Transitory API to preserve backwards-compatibility while refactoring. - - Once the legacy transaction management is fully deprecated, this should - simply be replaced by atomic. Until then, it's necessary to guarantee that - a commit occurs on exit, which atomic doesn't do when it's nested. - - Unlike atomic, savepoint defaults to False because that's closer to the - legacy behavior. - """ - connection = get_connection(using) - if connection.get_autocommit() or connection.in_atomic_block: - return atomic(using, savepoint) - else: - def entering(using): - pass - - def exiting(exc_type, using): - set_dirty(using=using) - - return _transaction_func(entering, exiting, using) diff --git a/django/middleware/transaction.py b/django/middleware/transaction.py deleted file mode 100644 index a95c4a89c2..0000000000 --- a/django/middleware/transaction.py +++ /dev/null @@ -1,58 +0,0 @@ -import warnings - -from django.core.exceptions import MiddlewareNotUsed -from django.db import connection, transaction -from django.utils.deprecation import RemovedInDjango18Warning - - -class TransactionMiddleware(object): - """ - Transaction middleware. If this is enabled, each view function will be run - with commit_on_response activated - that way a save() doesn't do a direct - commit, the commit is done when a successful response is created. If an - exception happens, the database is rolled back. - """ - - def __init__(self): - warnings.warn( - "TransactionMiddleware is deprecated in favor of ATOMIC_REQUESTS.", - RemovedInDjango18Warning, stacklevel=2) - if connection.settings_dict['ATOMIC_REQUESTS']: - raise MiddlewareNotUsed - - def process_request(self, request): - """Enters transaction management""" - transaction.enter_transaction_management() - - def process_exception(self, request, exception): - """Rolls back the database and leaves transaction management""" - if transaction.is_dirty(): - # This rollback might fail because of network failure for example. - # If rollback isn't possible it is impossible to clean the - # connection's state. So leave the connection in dirty state and - # let request_finished signal deal with cleaning the connection. - transaction.rollback() - transaction.leave_transaction_management() - - def process_response(self, request, response): - """Commits and leaves transaction management.""" - if not transaction.get_autocommit(): - if transaction.is_dirty(): - # Note: it is possible that the commit fails. If the reason is - # closed connection or some similar reason, then there is - # little hope to proceed nicely. However, in some cases ( - # deferred foreign key checks for exampl) it is still possible - # to rollback(). - try: - transaction.commit() - except Exception: - # If the rollback fails, the transaction state will be - # messed up. It doesn't matter, the connection will be set - # to clean state after the request finishes. And, we can't - # clean the state here properly even if we wanted to, the - # connection is in transaction but we can't rollback... - transaction.rollback() - transaction.leave_transaction_management() - raise - transaction.leave_transaction_management() - return response diff --git a/django/test/testcases.py b/django/test/testcases.py index 5b5555bfce..b495e64886 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -57,9 +57,6 @@ def to_list(value): real_commit = transaction.commit real_rollback = transaction.rollback -real_enter_transaction_management = transaction.enter_transaction_management -real_leave_transaction_management = transaction.leave_transaction_management -real_abort = transaction.abort def nop(*args, **kwargs): @@ -69,17 +66,11 @@ def nop(*args, **kwargs): def disable_transaction_methods(): transaction.commit = nop transaction.rollback = nop - transaction.enter_transaction_management = nop - transaction.leave_transaction_management = nop - transaction.abort = nop def restore_transaction_methods(): transaction.commit = real_commit transaction.rollback = real_rollback - transaction.enter_transaction_management = real_enter_transaction_management - transaction.leave_transaction_management = real_leave_transaction_management - transaction.abort = real_abort def assert_and_parse_html(self, html, user_msg, msg): @@ -772,7 +763,7 @@ class TransactionTestCase(SimpleTestCase): sql_list = conn.ops.sequence_reset_by_name_sql( no_style(), conn.introspection.sequence_list()) if sql_list: - with transaction.commit_on_success_unless_managed(using=db_name): + with transaction.atomic(using=db_name): cursor = conn.cursor() for sql in sql_list: cursor.execute(sql) diff --git a/tests/backends/tests.py b/tests/backends/tests.py index eaded5dbae..ab70a9e97e 100644 --- a/tests/backends/tests.py +++ b/tests/backends/tests.py @@ -371,7 +371,7 @@ class PostgresNewConnectionTests(TestCase): # Fetch a new connection with the new_tz as default # time zone, run a query and rollback. new_connection.settings_dict['TIME_ZONE'] = new_tz - new_connection.enter_transaction_management() + new_connection.set_autocommit(False) cursor = new_connection.cursor() new_connection.rollback() diff --git a/tests/basic/tests.py b/tests/basic/tests.py index 7b1f78a3b6..a8e4df32d6 100644 --- a/tests/basic/tests.py +++ b/tests/basic/tests.py @@ -755,7 +755,6 @@ class ConcurrentSaveTests(TransactionTestCase): try: # Do not delete a directly - doing so alters its state. Article.objects.filter(pk=a.pk).delete() - connections[DEFAULT_DB_ALIAS].commit_unless_managed() except Exception as e: exceptions.append(e) finally: diff --git a/tests/delete_regress/tests.py b/tests/delete_regress/tests.py index e1cbe7070f..0d33c9df9f 100644 --- a/tests/delete_regress/tests.py +++ b/tests/delete_regress/tests.py @@ -20,17 +20,17 @@ class DeleteLockingTest(TransactionTestCase): available_apps = ['delete_regress'] def setUp(self): + transaction.set_autocommit(False) # Create a second connection to the default database new_connections = ConnectionHandler(settings.DATABASES) self.conn2 = new_connections[DEFAULT_DB_ALIAS] - # Put both DB connections into managed transaction mode - transaction.enter_transaction_management() - self.conn2.enter_transaction_management() + self.conn2.set_autocommit(False) def tearDown(self): + transaction.rollback() + transaction.set_autocommit(True) # Close down the second connection. - transaction.leave_transaction_management() - self.conn2.abort() + self.conn2.rollback() self.conn2.close() @skipUnlessDBFeature('test_db_allows_multiple_connections') @@ -38,15 +38,10 @@ class DeleteLockingTest(TransactionTestCase): "Deletes on concurrent transactions don't collide and lock the database. Regression for #9479" # Create some dummy data - b1 = Book(id=1, pagecount=100) - b2 = Book(id=2, pagecount=200) - b3 = Book(id=3, pagecount=300) - b1.save() - b2.save() - b3.save() - - transaction.commit() - + with transaction.atomic(): + Book.objects.create(id=1, pagecount=100) + Book.objects.create(id=2, pagecount=200) + Book.objects.create(id=3, pagecount=300) self.assertEqual(3, Book.objects.count()) # Delete something using connection 2. @@ -58,10 +53,9 @@ class DeleteLockingTest(TransactionTestCase): # deleted in connection 2. This causes an infinite loop # under MySQL InnoDB unless we keep track of already # deleted objects. - Book.objects.filter(pagecount__lt=250).delete() - transaction.commit() + with transaction.atomic(): + Book.objects.filter(pagecount__lt=250).delete() self.assertEqual(1, Book.objects.count()) - transaction.commit() class DeleteCascadeTests(TestCase): diff --git a/tests/middleware/models.py b/tests/middleware/models.py deleted file mode 100644 index 7088bfc2f3..0000000000 --- a/tests/middleware/models.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.db import models -from django.utils.encoding import python_2_unicode_compatible - - -@python_2_unicode_compatible -class Band(models.Model): - name = models.CharField(max_length=100) - - class Meta: - ordering = ('name',) - - def __str__(self): - return self.name diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py index ffe71d1e4d..6974cd9ad7 100644 --- a/tests/middleware/tests.py +++ b/tests/middleware/tests.py @@ -9,22 +9,16 @@ from unittest import skipIf from django.conf import settings from django.core import mail -from django.db import (transaction, connections, DEFAULT_DB_ALIAS, - IntegrityError) from django.http import HttpRequest, HttpResponse, StreamingHttpResponse from django.middleware.clickjacking import XFrameOptionsMiddleware from django.middleware.common import CommonMiddleware, BrokenLinkEmailsMiddleware from django.middleware.http import ConditionalGetMiddleware from django.middleware.gzip import GZipMiddleware -from django.middleware.transaction import TransactionMiddleware -from django.test import TransactionTestCase, TestCase, RequestFactory, override_settings -from django.test.utils import IgnoreDeprecationWarningsMixin +from django.test import TestCase, RequestFactory, override_settings from django.utils import six from django.utils.encoding import force_str from django.utils.six.moves import xrange -from .models import Band - class CommonMiddlewareTest(TestCase): urls = 'middleware.urls' @@ -666,64 +660,3 @@ class ETagGZipMiddlewareTest(TestCase): nogzip_etag = response.get('ETag') self.assertNotEqual(gzip_etag, nogzip_etag) - - -class TransactionMiddlewareTest(IgnoreDeprecationWarningsMixin, TransactionTestCase): - """ - Test the transaction middleware. - """ - - available_apps = ['middleware'] - - def setUp(self): - super(TransactionMiddlewareTest, self).setUp() - self.request = HttpRequest() - self.request.META = { - 'SERVER_NAME': 'testserver', - 'SERVER_PORT': 80, - } - self.request.path = self.request.path_info = "/" - self.response = HttpResponse() - self.response.status_code = 200 - - def tearDown(self): - transaction.abort() - super(TransactionMiddlewareTest, self).tearDown() - - def test_request(self): - TransactionMiddleware().process_request(self.request) - self.assertFalse(transaction.get_autocommit()) - - def test_managed_response(self): - transaction.enter_transaction_management() - Band.objects.create(name='The Beatles') - self.assertTrue(transaction.is_dirty()) - TransactionMiddleware().process_response(self.request, self.response) - self.assertFalse(transaction.is_dirty()) - self.assertEqual(Band.objects.count(), 1) - - def test_exception(self): - transaction.enter_transaction_management() - Band.objects.create(name='The Beatles') - self.assertTrue(transaction.is_dirty()) - TransactionMiddleware().process_exception(self.request, None) - self.assertFalse(transaction.is_dirty()) - self.assertEqual(Band.objects.count(), 0) - - def test_failing_commit(self): - # It is possible that connection.commit() fails. Check that - # TransactionMiddleware handles such cases correctly. - try: - def raise_exception(): - raise IntegrityError() - connections[DEFAULT_DB_ALIAS].commit = raise_exception - transaction.enter_transaction_management() - Band.objects.create(name='The Beatles') - self.assertTrue(transaction.is_dirty()) - with self.assertRaises(IntegrityError): - TransactionMiddleware().process_response(self.request, None) - self.assertFalse(transaction.is_dirty()) - self.assertEqual(Band.objects.count(), 0) - self.assertFalse(transaction.is_managed()) - finally: - del connections[DEFAULT_DB_ALIAS].commit diff --git a/tests/requests/tests.py b/tests/requests/tests.py index 3baee7994b..cf90fb1f1b 100644 --- a/tests/requests/tests.py +++ b/tests/requests/tests.py @@ -5,15 +5,12 @@ from datetime import datetime, timedelta from io import BytesIO from itertools import chain import time -from unittest import skipIf -from django.db import connection, connections -from django.core import signals from django.core.exceptions import SuspiciousOperation from django.core.handlers.wsgi import WSGIRequest, LimitedStream from django.http import (HttpRequest, HttpResponse, parse_cookie, build_request_repr, UnreadablePostError, RawPostDataException) -from django.test import SimpleTestCase, TransactionTestCase, override_settings +from django.test import SimpleTestCase, override_settings from django.test.client import FakePayload from django.test.utils import str_prefix from django.utils import six @@ -696,60 +693,3 @@ class HostValidationTests(SimpleTestCase): msg_suggestion2 % "invalid_hostname.com", request.get_host ) - - -@skipIf(connection.vendor == 'sqlite' - and connection.settings_dict['TEST']['NAME'] in (None, '', ':memory:'), - "Cannot establish two connections to an in-memory SQLite database.") -class DatabaseConnectionHandlingTests(TransactionTestCase): - - available_apps = [] - - def setUp(self): - # Use a temporary connection to avoid messing with the main one. - self._old_default_connection = connections['default'] - del connections['default'] - - def tearDown(self): - try: - connections['default'].close() - finally: - connections['default'] = self._old_default_connection - - def test_request_finished_db_state(self): - # Force closing connection on request end - connection.settings_dict['CONN_MAX_AGE'] = 0 - - # The GET below will not succeed, but it will give a response with - # defined ._handler_class. That is needed for sending the - # request_finished signal. - response = self.client.get('/') - # Make sure there is an open connection - connection.ensure_connection() - connection.enter_transaction_management() - signals.request_finished.send(sender=response._handler_class) - self.assertEqual(len(connection.transaction_state), 0) - - def test_request_finished_failed_connection(self): - # Force closing connection on request end - connection.settings_dict['CONN_MAX_AGE'] = 0 - - connection.enter_transaction_management() - connection.set_dirty() - - # Test that the rollback doesn't succeed (for example network failure - # could cause this). - def fail_horribly(): - raise Exception("Horrible failure!") - connection._rollback = fail_horribly - try: - with self.assertRaises(Exception): - signals.request_finished.send(sender=self.__class__) - # The connection's state wasn't cleaned up - self.assertEqual(len(connection.transaction_state), 1) - finally: - del connection._rollback - # The connection will be cleaned on next request where the conn - # works again. - signals.request_finished.send(sender=self.__class__) - self.assertEqual(len(connection.transaction_state), 0) diff --git a/tests/select_for_update/tests.py b/tests/select_for_update/tests.py index 278458ed39..a4136d7a16 100644 --- a/tests/select_for_update/tests.py +++ b/tests/select_for_update/tests.py @@ -31,34 +31,24 @@ class SelectForUpdateTests(TransactionTestCase): available_apps = ['select_for_update'] def setUp(self): - transaction.enter_transaction_management() + # This is executed in autocommit mode so that code in + # run_select_for_update can see this data. self.person = Person.objects.create(name='Reinhardt') - # We have to commit here so that code in run_select_for_update can - # see this data. - transaction.commit() - - # We need another database connection to test that one connection - # issuing a SELECT ... FOR UPDATE will block. + # We need another database connection in transaction to test that one + # connection issuing a SELECT ... FOR UPDATE will block. new_connections = ConnectionHandler(settings.DATABASES) self.new_connection = new_connections[DEFAULT_DB_ALIAS] - self.new_connection.enter_transaction_management() def tearDown(self): - try: - # We don't really care if this fails - some of the tests will set - # this in the course of their run. - transaction.abort() - self.new_connection.abort() - except transaction.TransactionManagementError: - pass - self.new_connection.close() try: self.end_blocking_transaction() except (DatabaseError, AttributeError): pass + self.new_connection.close() def start_blocking_transaction(self): + self.new_connection.set_autocommit(False) # Start a blocking transaction. At some point, # end_blocking_transaction() should be called. self.cursor = self.new_connection.cursor() @@ -72,6 +62,7 @@ class SelectForUpdateTests(TransactionTestCase): def end_blocking_transaction(self): # Roll back the blocking transaction. self.new_connection.rollback() + self.new_connection.set_autocommit(True) def has_for_update_sql(self, tested_connection, nowait=False): # Examine the SQL that was executed to determine whether it @@ -146,19 +137,17 @@ class SelectForUpdateTests(TransactionTestCase): try: # We need to enter transaction management again, as this is done on # per-thread basis - transaction.enter_transaction_management() - people = list( - Person.objects.all().select_for_update(nowait=nowait) - ) - people[0].name = 'Fred' - people[0].save() - transaction.commit() + with transaction.atomic(): + people = list( + Person.objects.all().select_for_update(nowait=nowait) + ) + people[0].name = 'Fred' + people[0].save() except DatabaseError as e: status.append(e) finally: # This method is run in a separate thread. It uses its own # database connection. Close it without waiting for the GC. - transaction.abort() connection.close() @requires_threading @@ -245,16 +234,6 @@ class SelectForUpdateTests(TransactionTestCase): self.end_blocking_transaction() self.assertIsInstance(status[-1], DatabaseError) - @skipUnlessDBFeature('has_select_for_update') - def test_transaction_dirty_managed(self): - """ Check that a select_for_update sets the transaction to be - dirty when executed under txn management. Setting the txn dirty - means that it will be either committed or rolled back by Django, - which will release any locks held by the SELECT FOR UPDATE. - """ - list(Person.objects.select_for_update()) - self.assertTrue(transaction.is_dirty()) - @skipUnlessDBFeature('has_select_for_update') def test_select_for_update_on_multidb(self): old_routers = router.routers diff --git a/tests/serializers/tests.py b/tests/serializers/tests.py index 6433101878..941a231841 100644 --- a/tests/serializers/tests.py +++ b/tests/serializers/tests.py @@ -272,16 +272,13 @@ class SerializersTransactionTestBase(object): Tests that objects ids can be referenced before they are defined in the serialization data. """ - # The deserialization process needs to be contained - # within a transaction in order to test forward reference - # handling. - transaction.enter_transaction_management() - objs = serializers.deserialize(self.serializer_name, self.fwd_ref_str) - with connection.constraint_checks_disabled(): - for obj in objs: - obj.save() - transaction.commit() - transaction.leave_transaction_management() + # The deserialization process needs to run in a transaction in order + # to test forward reference handling. + with transaction.atomic(): + objs = serializers.deserialize(self.serializer_name, self.fwd_ref_str) + with connection.constraint_checks_disabled(): + for obj in objs: + obj.save() for model_cls in (Category, Author, Article): self.assertEqual(model_cls.objects.all().count(), 1) diff --git a/tests/transactions/tests.py b/tests/transactions/tests.py index e7ce43cd93..8b2e67a3cb 100644 --- a/tests/transactions/tests.py +++ b/tests/transactions/tests.py @@ -4,8 +4,7 @@ import sys from unittest import skipIf, skipUnless from django.db import connection, transaction, DatabaseError, IntegrityError -from django.test import TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature -from django.test.utils import IgnoreDeprecationWarningsMixin +from django.test import TransactionTestCase, skipIfDBFeature from django.utils import six from .models import Reporter @@ -238,20 +237,6 @@ class AtomicWithoutAutocommitTests(AtomicTests): transaction.set_autocommit(True) -@skipIf(connection.features.autocommits_when_autocommit_is_off, - "This test requires a non-autocommit mode that doesn't autocommit.") -class AtomicInsideLegacyTransactionManagementTests(AtomicTests): - - def setUp(self): - transaction.enter_transaction_management() - - def tearDown(self): - # The tests access the database after exercising 'atomic', making the - # connection dirty; a rollback is required to make it clean. - transaction.rollback() - transaction.leave_transaction_management() - - @skipUnless(connection.features.uses_savepoints, "'atomic' requires transactions and savepoints.") class AtomicMergeTests(TransactionTestCase): @@ -324,13 +309,6 @@ class AtomicErrorsTests(TransactionTestCase): with self.assertRaises(transaction.TransactionManagementError): transaction.rollback() - def test_atomic_prevents_calling_transaction_management_methods(self): - with transaction.atomic(): - with self.assertRaises(transaction.TransactionManagementError): - transaction.enter_transaction_management() - with self.assertRaises(transaction.TransactionManagementError): - transaction.leave_transaction_management() - def test_atomic_prevents_queries_in_broken_transaction(self): r1 = Reporter.objects.create(first_name="Archibald", last_name="Haddock") with transaction.atomic(): @@ -366,319 +344,3 @@ class AtomicMiscTests(TransactionTestCase): pass # Must not raise an exception transaction.atomic(Callable()) - - -class TransactionTests(IgnoreDeprecationWarningsMixin, TransactionTestCase): - - available_apps = ['transactions'] - - def create_a_reporter_then_fail(self, first, last): - a = Reporter(first_name=first, last_name=last) - a.save() - raise Exception("I meant to do that") - - def remove_a_reporter(self, first_name): - r = Reporter.objects.get(first_name="Alice") - r.delete() - - def manually_managed(self): - r = Reporter(first_name="Dirk", last_name="Gently") - r.save() - transaction.commit() - - def manually_managed_mistake(self): - r = Reporter(first_name="Edward", last_name="Woodward") - r.save() - # Oops, I forgot to commit/rollback! - - @skipUnlessDBFeature('supports_transactions') - def test_autocommit(self): - """ - The default behavior is to autocommit after each save() action. - """ - self.assertRaises( - Exception, - self.create_a_reporter_then_fail, - "Alice", "Smith" - ) - - # The object created before the exception still exists - self.assertEqual(Reporter.objects.count(), 1) - - @skipUnlessDBFeature('supports_transactions') - def test_autocommit_decorator(self): - """ - The autocommit decorator works exactly the same as the default behavior. - """ - autocomitted_create_then_fail = transaction.autocommit( - self.create_a_reporter_then_fail - ) - self.assertRaises( - Exception, - autocomitted_create_then_fail, - "Alice", "Smith" - ) - # Again, the object created before the exception still exists - self.assertEqual(Reporter.objects.count(), 1) - - @skipUnlessDBFeature('supports_transactions') - def test_autocommit_decorator_with_using(self): - """ - The autocommit decorator also works with a using argument. - """ - autocomitted_create_then_fail = transaction.autocommit(using='default')( - self.create_a_reporter_then_fail - ) - self.assertRaises( - Exception, - autocomitted_create_then_fail, - "Alice", "Smith" - ) - # Again, the object created before the exception still exists - self.assertEqual(Reporter.objects.count(), 1) - - @skipUnlessDBFeature('supports_transactions') - def test_commit_on_success(self): - """ - With the commit_on_success decorator, the transaction is only committed - if the function doesn't throw an exception. - """ - committed_on_success = transaction.commit_on_success( - self.create_a_reporter_then_fail) - self.assertRaises(Exception, committed_on_success, "Dirk", "Gently") - # This time the object never got saved - self.assertEqual(Reporter.objects.count(), 0) - - @skipUnlessDBFeature('supports_transactions') - def test_commit_on_success_with_using(self): - """ - The commit_on_success decorator also works with a using argument. - """ - using_committed_on_success = transaction.commit_on_success(using='default')( - self.create_a_reporter_then_fail - ) - self.assertRaises( - Exception, - using_committed_on_success, - "Dirk", "Gently" - ) - # This time the object never got saved - self.assertEqual(Reporter.objects.count(), 0) - - @skipUnlessDBFeature('supports_transactions') - def test_commit_on_success_succeed(self): - """ - If there aren't any exceptions, the data will get saved. - """ - Reporter.objects.create(first_name="Alice", last_name="Smith") - remove_comitted_on_success = transaction.commit_on_success( - self.remove_a_reporter - ) - remove_comitted_on_success("Alice") - self.assertEqual(list(Reporter.objects.all()), []) - - @skipUnlessDBFeature('supports_transactions') - def test_commit_on_success_exit(self): - @transaction.autocommit() - def gen_reporter(): - @transaction.commit_on_success - def create_reporter(): - Reporter.objects.create(first_name="Bobby", last_name="Tables") - - create_reporter() - # Much more formal - r = Reporter.objects.get() - r.first_name = "Robert" - r.save() - - gen_reporter() - r = Reporter.objects.get() - self.assertEqual(r.first_name, "Robert") - - @skipUnlessDBFeature('supports_transactions') - def test_manually_managed(self): - """ - You can manually manage transactions if you really want to, but you - have to remember to commit/rollback. - """ - manually_managed = transaction.commit_manually(self.manually_managed) - manually_managed() - self.assertEqual(Reporter.objects.count(), 1) - - @skipUnlessDBFeature('supports_transactions') - def test_manually_managed_mistake(self): - """ - If you forget, you'll get bad errors. - """ - manually_managed_mistake = transaction.commit_manually( - self.manually_managed_mistake - ) - self.assertRaises(transaction.TransactionManagementError, - manually_managed_mistake) - - @skipUnlessDBFeature('supports_transactions') - def test_manually_managed_with_using(self): - """ - The commit_manually function also works with a using argument. - """ - using_manually_managed_mistake = transaction.commit_manually(using='default')( - self.manually_managed_mistake - ) - self.assertRaises( - transaction.TransactionManagementError, - using_manually_managed_mistake - ) - - -class TransactionRollbackTests(IgnoreDeprecationWarningsMixin, TransactionTestCase): - - available_apps = ['transactions'] - - def execute_bad_sql(self): - with connection.cursor() as cursor: - cursor.execute("INSERT INTO transactions_reporter (first_name, last_name) VALUES ('Douglas', 'Adams');") - - @skipUnlessDBFeature('requires_rollback_on_dirty_transaction') - def test_bad_sql(self): - """ - Regression for #11900: If a function wrapped by commit_on_success - writes a transaction that can't be committed, that transaction should - be rolled back. The bug is only visible using the psycopg2 backend, - though the fix is generally a good idea. - """ - execute_bad_sql = transaction.commit_on_success(self.execute_bad_sql) - self.assertRaises(IntegrityError, execute_bad_sql) - transaction.rollback() - - -class TransactionContextManagerTests(IgnoreDeprecationWarningsMixin, TransactionTestCase): - - available_apps = ['transactions'] - - def create_reporter_and_fail(self): - Reporter.objects.create(first_name="Bob", last_name="Holtzman") - raise Exception - - @skipUnlessDBFeature('supports_transactions') - def test_autocommit(self): - """ - The default behavior is to autocommit after each save() action. - """ - with self.assertRaises(Exception): - self.create_reporter_and_fail() - # The object created before the exception still exists - self.assertEqual(Reporter.objects.count(), 1) - - @skipUnlessDBFeature('supports_transactions') - def test_autocommit_context_manager(self): - """ - The autocommit context manager works exactly the same as the default - behavior. - """ - with self.assertRaises(Exception): - with transaction.autocommit(): - self.create_reporter_and_fail() - - self.assertEqual(Reporter.objects.count(), 1) - - @skipUnlessDBFeature('supports_transactions') - def test_autocommit_context_manager_with_using(self): - """ - The autocommit context manager also works with a using argument. - """ - with self.assertRaises(Exception): - with transaction.autocommit(using="default"): - self.create_reporter_and_fail() - - self.assertEqual(Reporter.objects.count(), 1) - - @skipUnlessDBFeature('supports_transactions') - def test_commit_on_success(self): - """ - With the commit_on_success context manager, the transaction is only - committed if the block doesn't throw an exception. - """ - with self.assertRaises(Exception): - with transaction.commit_on_success(): - self.create_reporter_and_fail() - - self.assertEqual(Reporter.objects.count(), 0) - - @skipUnlessDBFeature('supports_transactions') - def test_commit_on_success_with_using(self): - """ - The commit_on_success context manager also works with a using argument. - """ - with self.assertRaises(Exception): - with transaction.commit_on_success(using="default"): - self.create_reporter_and_fail() - - self.assertEqual(Reporter.objects.count(), 0) - - @skipUnlessDBFeature('supports_transactions') - def test_commit_on_success_succeed(self): - """ - If there aren't any exceptions, the data will get saved. - """ - Reporter.objects.create(first_name="Alice", last_name="Smith") - with transaction.commit_on_success(): - Reporter.objects.filter(first_name="Alice").delete() - - self.assertQuerysetEqual(Reporter.objects.all(), []) - - @skipUnlessDBFeature('supports_transactions') - def test_commit_on_success_exit(self): - with transaction.autocommit(): - with transaction.commit_on_success(): - Reporter.objects.create(first_name="Bobby", last_name="Tables") - - # Much more formal - r = Reporter.objects.get() - r.first_name = "Robert" - r.save() - - r = Reporter.objects.get() - self.assertEqual(r.first_name, "Robert") - - @skipUnlessDBFeature('supports_transactions') - def test_manually_managed(self): - """ - You can manually manage transactions if you really want to, but you - have to remember to commit/rollback. - """ - with transaction.commit_manually(): - Reporter.objects.create(first_name="Libby", last_name="Holtzman") - transaction.commit() - self.assertEqual(Reporter.objects.count(), 1) - - @skipUnlessDBFeature('supports_transactions') - def test_manually_managed_mistake(self): - """ - If you forget, you'll get bad errors. - """ - with self.assertRaises(transaction.TransactionManagementError): - with transaction.commit_manually(): - Reporter.objects.create(first_name="Scott", last_name="Browning") - - @skipUnlessDBFeature('supports_transactions') - def test_manually_managed_with_using(self): - """ - The commit_manually function also works with a using argument. - """ - with self.assertRaises(transaction.TransactionManagementError): - with transaction.commit_manually(using="default"): - Reporter.objects.create(first_name="Walter", last_name="Cronkite") - - @skipUnlessDBFeature('requires_rollback_on_dirty_transaction') - def test_bad_sql(self): - """ - Regression for #11900: If a block wrapped by commit_on_success - writes a transaction that can't be committed, that transaction should - be rolled back. The bug is only visible using the psycopg2 backend, - though the fix is generally a good idea. - """ - with self.assertRaises(IntegrityError): - with transaction.commit_on_success(): - with connection.cursor() as cursor: - cursor.execute("INSERT INTO transactions_reporter (first_name, last_name) VALUES ('Douglas', 'Adams');") - transaction.rollback() diff --git a/tests/transactions_regress/__init__.py b/tests/transactions_regress/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/transactions_regress/models.py b/tests/transactions_regress/models.py deleted file mode 100644 index 32234d9ba4..0000000000 --- a/tests/transactions_regress/models.py +++ /dev/null @@ -1,17 +0,0 @@ -from django.db import models - - -class Mod(models.Model): - fld = models.IntegerField() - - -class SubMod(Mod): - cnt = models.IntegerField(unique=True) - - -class M2mA(models.Model): - others = models.ManyToManyField('M2mB') - - -class M2mB(models.Model): - fld = models.IntegerField() diff --git a/tests/transactions_regress/tests.py b/tests/transactions_regress/tests.py deleted file mode 100644 index 917aafb3c8..0000000000 --- a/tests/transactions_regress/tests.py +++ /dev/null @@ -1,392 +0,0 @@ -from __future__ import unicode_literals - -from unittest import skipIf, skipUnless, SkipTest - -from django.db import (connection, connections, transaction, DEFAULT_DB_ALIAS, DatabaseError, - IntegrityError) -from django.db.transaction import commit_on_success, commit_manually, TransactionManagementError -from django.test import TransactionTestCase, override_settings, skipUnlessDBFeature -from django.test.utils import IgnoreDeprecationWarningsMixin - -from .models import Mod, M2mA, M2mB, SubMod - - -class ModelInheritanceTests(TransactionTestCase): - - available_apps = ['transactions_regress'] - - def test_save(self): - # First, create a SubMod, then try to save another with conflicting - # cnt field. The problem was that transactions were committed after - # every parent save when not in managed transaction. As the cnt - # conflict is in the second model, we can check if the first save - # was committed or not. - SubMod(fld=1, cnt=1).save() - # We should have committed the transaction for the above - assert this. - connection.rollback() - self.assertEqual(SubMod.objects.count(), 1) - try: - SubMod(fld=2, cnt=1).save() - except IntegrityError: - connection.rollback() - self.assertEqual(SubMod.objects.count(), 1) - self.assertEqual(Mod.objects.count(), 1) - - -class TestTransactionClosing(IgnoreDeprecationWarningsMixin, TransactionTestCase): - """ - Tests to make sure that transactions are properly closed - when they should be, and aren't left pending after operations - have been performed in them. Refs #9964. - """ - - available_apps = [ - 'transactions_regress', - 'django.contrib.auth', - 'django.contrib.contenttypes', - ] - - def test_raw_committed_on_success(self): - """ - Make sure a transaction consisting of raw SQL execution gets - committed by the commit_on_success decorator. - """ - @commit_on_success - def raw_sql(): - "Write a record using raw sql under a commit_on_success decorator" - with connection.cursor() as cursor: - cursor.execute("INSERT into transactions_regress_mod (fld) values (18)") - - raw_sql() - # Rollback so that if the decorator didn't commit, the record is unwritten - transaction.rollback() - self.assertEqual(Mod.objects.count(), 1) - # Check that the record is in the DB - obj = Mod.objects.all()[0] - self.assertEqual(obj.fld, 18) - - def test_commit_manually_enforced(self): - """ - Make sure that under commit_manually, even "read-only" transaction require closure - (commit or rollback), and a transaction left pending is treated as an error. - """ - @commit_manually - def non_comitter(): - "Execute a managed transaction with read-only operations and fail to commit" - Mod.objects.count() - - self.assertRaises(TransactionManagementError, non_comitter) - - def test_commit_manually_commit_ok(self): - """ - Test that under commit_manually, a committed transaction is accepted by the transaction - management mechanisms - """ - @commit_manually - def committer(): - """ - Perform a database query, then commit the transaction - """ - Mod.objects.count() - transaction.commit() - - try: - committer() - except TransactionManagementError: - self.fail("Commit did not clear the transaction state") - - def test_commit_manually_rollback_ok(self): - """ - Test that under commit_manually, a rolled-back transaction is accepted by the transaction - management mechanisms - """ - @commit_manually - def roller_back(): - """ - Perform a database query, then rollback the transaction - """ - Mod.objects.count() - transaction.rollback() - - try: - roller_back() - except TransactionManagementError: - self.fail("Rollback did not clear the transaction state") - - def test_commit_manually_enforced_after_commit(self): - """ - Test that under commit_manually, if a transaction is committed and an operation is - performed later, we still require the new transaction to be closed - """ - @commit_manually - def fake_committer(): - "Query, commit, then query again, leaving with a pending transaction" - Mod.objects.count() - transaction.commit() - Mod.objects.count() - - self.assertRaises(TransactionManagementError, fake_committer) - - @skipUnlessDBFeature('supports_transactions') - def test_reuse_cursor_reference(self): - """ - Make sure transaction closure is enforced even when the queries are performed - through a single cursor reference retrieved in the beginning - (this is to show why it is wrong to set the transaction dirty only when a cursor - is fetched from the connection). - """ - @commit_on_success - def reuse_cursor_ref(): - """ - Fetch a cursor, perform an query, rollback to close the transaction, - then write a record (in a new transaction) using the same cursor object - (reference). All this under commit_on_success, so the second insert should - be committed. - """ - with connection.cursor() as cursor: - cursor.execute("INSERT into transactions_regress_mod (fld) values (2)") - transaction.rollback() - cursor.execute("INSERT into transactions_regress_mod (fld) values (2)") - - reuse_cursor_ref() - # Rollback so that if the decorator didn't commit, the record is unwritten - transaction.rollback() - self.assertEqual(Mod.objects.count(), 1) - obj = Mod.objects.all()[0] - self.assertEqual(obj.fld, 2) - - def test_failing_query_transaction_closed(self): - """ - Make sure that under commit_on_success, a transaction is rolled back even if - the first database-modifying operation fails. - This is prompted by http://code.djangoproject.com/ticket/6669 (and based on sample - code posted there to exemplify the problem): Before Django 1.3, - transactions were only marked "dirty" by the save() function after it successfully - wrote the object to the database. - """ - from django.contrib.auth.models import User - - @transaction.commit_on_success - def create_system_user(): - "Create a user in a transaction" - user = User.objects.create_user(username='system', password='iamr00t', - email='root@SITENAME.com') - # Redundant, just makes sure the user id was read back from DB - Mod.objects.create(fld=user.pk) - - # Create a user - create_system_user() - - with self.assertRaises(DatabaseError): - # The second call to create_system_user should fail for violating - # a unique constraint (it's trying to re-create the same user) - create_system_user() - - # Try to read the database. If the last transaction was indeed closed, - # this should cause no problems - User.objects.all()[0] - - @override_settings(DEBUG=True) - def test_failing_query_transaction_closed_debug(self): - """ - Regression for #6669. Same test as above, with DEBUG=True. - """ - self.test_failing_query_transaction_closed() - - -@skipIf(connection.vendor == 'sqlite' - and connection.settings_dict['TEST']['NAME'] in (None, '', ':memory:'), - "Cannot establish two connections to an in-memory SQLite database.") -class TestNewConnection(IgnoreDeprecationWarningsMixin, TransactionTestCase): - """ - Check that new connections don't have special behavior. - """ - - available_apps = ['transactions_regress'] - - def setUp(self): - self._old_backend = connections[DEFAULT_DB_ALIAS] - settings = self._old_backend.settings_dict.copy() - new_backend = self._old_backend.__class__(settings, DEFAULT_DB_ALIAS) - connections[DEFAULT_DB_ALIAS] = new_backend - - def tearDown(self): - try: - connections[DEFAULT_DB_ALIAS].abort() - connections[DEFAULT_DB_ALIAS].close() - finally: - connections[DEFAULT_DB_ALIAS] = self._old_backend - - def test_commit(self): - """ - Users are allowed to commit and rollback connections. - """ - connection.set_autocommit(False) - try: - # The starting value is False, not None. - self.assertIs(connection._dirty, False) - list(Mod.objects.all()) - self.assertTrue(connection.is_dirty()) - connection.commit() - self.assertFalse(connection.is_dirty()) - list(Mod.objects.all()) - self.assertTrue(connection.is_dirty()) - connection.rollback() - self.assertFalse(connection.is_dirty()) - finally: - connection.set_autocommit(True) - - def test_enter_exit_management(self): - orig_dirty = connection._dirty - connection.enter_transaction_management() - connection.leave_transaction_management() - self.assertEqual(orig_dirty, connection._dirty) - - -@skipUnless(connection.vendor == 'postgresql', - "This test only valid for PostgreSQL") -class TestPostgresAutocommitAndIsolation(IgnoreDeprecationWarningsMixin, TransactionTestCase): - """ - Tests to make sure psycopg2's autocommit mode and isolation level - is restored after entering and leaving transaction management. - Refs #16047, #18130. - """ - - available_apps = ['transactions_regress'] - - def setUp(self): - from psycopg2.extensions import (ISOLATION_LEVEL_AUTOCOMMIT, - ISOLATION_LEVEL_SERIALIZABLE, - TRANSACTION_STATUS_IDLE) - self._autocommit = ISOLATION_LEVEL_AUTOCOMMIT - self._serializable = ISOLATION_LEVEL_SERIALIZABLE - self._idle = TRANSACTION_STATUS_IDLE - - # We want a clean backend with autocommit = True, so - # first we need to do a bit of work to have that. - self._old_backend = connections[DEFAULT_DB_ALIAS] - settings = self._old_backend.settings_dict.copy() - opts = settings['OPTIONS'].copy() - opts['isolation_level'] = ISOLATION_LEVEL_SERIALIZABLE - settings['OPTIONS'] = opts - new_backend = self._old_backend.__class__(settings, DEFAULT_DB_ALIAS) - connections[DEFAULT_DB_ALIAS] = new_backend - - def tearDown(self): - try: - connections[DEFAULT_DB_ALIAS].abort() - finally: - connections[DEFAULT_DB_ALIAS].close() - connections[DEFAULT_DB_ALIAS] = self._old_backend - - def test_initial_autocommit_state(self): - # Autocommit is activated when the connection is created. - connection.cursor().close() - self.assertTrue(connection.autocommit) - - def test_transaction_management(self): - transaction.enter_transaction_management() - self.assertFalse(connection.autocommit) - self.assertEqual(connection.isolation_level, self._serializable) - - transaction.leave_transaction_management() - self.assertTrue(connection.autocommit) - - def test_transaction_stacking(self): - transaction.enter_transaction_management() - self.assertFalse(connection.autocommit) - self.assertEqual(connection.isolation_level, self._serializable) - - transaction.enter_transaction_management() - self.assertFalse(connection.autocommit) - self.assertEqual(connection.isolation_level, self._serializable) - - transaction.leave_transaction_management() - self.assertFalse(connection.autocommit) - self.assertEqual(connection.isolation_level, self._serializable) - - transaction.leave_transaction_management() - self.assertTrue(connection.autocommit) - - def test_enter_autocommit(self): - transaction.enter_transaction_management() - self.assertFalse(connection.autocommit) - self.assertEqual(connection.isolation_level, self._serializable) - list(Mod.objects.all()) - self.assertTrue(transaction.is_dirty()) - # Enter autocommit mode again. - transaction.enter_transaction_management(False) - self.assertFalse(transaction.is_dirty()) - self.assertEqual( - connection.connection.get_transaction_status(), - self._idle) - list(Mod.objects.all()) - self.assertFalse(transaction.is_dirty()) - transaction.leave_transaction_management() - self.assertFalse(connection.autocommit) - self.assertEqual(connection.isolation_level, self._serializable) - transaction.leave_transaction_management() - self.assertTrue(connection.autocommit) - - -class TestManyToManyAddTransaction(IgnoreDeprecationWarningsMixin, TransactionTestCase): - - available_apps = ['transactions_regress'] - - def test_manyrelated_add_commit(self): - "Test for https://code.djangoproject.com/ticket/16818" - a = M2mA.objects.create() - b = M2mB.objects.create(fld=10) - a.others.add(b) - - # We're in a TransactionTestCase and have not changed transaction - # behavior from default of "autocommit", so this rollback should not - # actually do anything. If it does in fact undo our add, that's a bug - # that the bulk insert was not auto-committed. - transaction.rollback() - self.assertEqual(a.others.count(), 1) - - -class SavepointTest(IgnoreDeprecationWarningsMixin, TransactionTestCase): - - available_apps = ['transactions_regress'] - - @skipIf(connection.vendor == 'sqlite', - "SQLite doesn't support savepoints in managed mode") - @skipUnlessDBFeature('uses_savepoints') - def test_savepoint_commit(self): - @commit_manually - def work(): - mod = Mod.objects.create(fld=1) - pk = mod.pk - sid = transaction.savepoint() - Mod.objects.filter(pk=pk).update(fld=10) - transaction.savepoint_commit(sid) - mod2 = Mod.objects.get(pk=pk) - transaction.commit() - self.assertEqual(mod2.fld, 10) - - work() - - @skipIf(connection.vendor == 'sqlite', - "SQLite doesn't support savepoints in managed mode") - @skipUnlessDBFeature('uses_savepoints') - def test_savepoint_rollback(self): - # _mysql_storage_engine issues a query and as such can't be applied in - # a skipIf decorator since that would execute the query on module load. - if (connection.vendor == 'mysql' and - connection.features._mysql_storage_engine == 'MyISAM'): - raise SkipTest("MyISAM MySQL storage engine doesn't support savepoints") - - @commit_manually - def work(): - mod = Mod.objects.create(fld=1) - pk = mod.pk - sid = transaction.savepoint() - Mod.objects.filter(pk=pk).update(fld=20) - transaction.savepoint_rollback(sid) - mod2 = Mod.objects.get(pk=pk) - transaction.commit() - self.assertEqual(mod2.fld, 1) - - work()