From 4b31a6a9e698a26e3e359e2ccf3da1505d114cf1 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 4 Mar 2013 15:57:04 +0100 Subject: [PATCH] Added support for savepoints in SQLite. Technically speaking they aren't usable yet. --- django/db/backends/sqlite3/base.py | 10 +++++++++ docs/ref/databases.txt | 3 +-- docs/topics/db/transactions.txt | 35 ++++++++++++++++++++--------- tests/transactions_regress/tests.py | 4 ++++ 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 9a37dd17fef..f537860a531 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -100,6 +100,10 @@ class DatabaseFeatures(BaseDatabaseFeatures): has_bulk_insert = True can_combine_inserts_with_and_without_auto_increment_pk = False + @cached_property + def uses_savepoints(self): + return Database.sqlite_version_info >= (3, 6, 8) + @cached_property def supports_stddev(self): """Confirm support for STDDEV and related stats functions @@ -355,6 +359,12 @@ class DatabaseWrapper(BaseDatabaseWrapper): if self.settings_dict['NAME'] != ":memory:": BaseDatabaseWrapper.close(self) + def _savepoint_allowed(self): + # When 'isolation_level' is None, Django doesn't provide a way to + # create a transaction (yet) so savepoints can't be created. When it + # isn't, sqlite3 commits before each savepoint -- it's a bug. + return False + def _set_autocommit(self, autocommit): if autocommit: level = None diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index 4dafb3774f6..78c1bb3dda1 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -424,8 +424,7 @@ Savepoints Both the Django ORM and MySQL (when using the InnoDB :ref:`storage engine `) support database :ref:`savepoints -`, but this feature wasn't available in -Django until version 1.4 when such support was added. +`. If you use the MyISAM storage engine please be aware of the fact that you will receive database-generated errors if you try to use the :ref:`savepoint-related diff --git a/docs/topics/db/transactions.txt b/docs/topics/db/transactions.txt index 93c4a3b11dd..e2c8e4e3f52 100644 --- a/docs/topics/db/transactions.txt +++ b/docs/topics/db/transactions.txt @@ -251,11 +251,11 @@ the transaction middleware, and only modify selected functions as needed. Savepoints ========== -A savepoint is a marker within a transaction that enables you to roll back part -of a transaction, rather than the full transaction. Savepoints are available -with the PostgreSQL 8, Oracle and MySQL (when using the InnoDB storage engine) -backends. Other backends provide the savepoint functions, but they're empty -operations -- they don't actually do anything. +A savepoint is a marker within a transaction that enables you to roll back +part of a transaction, rather than the full transaction. Savepoints are +available with the SQLite (≥ 3.6.8), PostgreSQL, Oracle and MySQL (when using +the InnoDB storage engine) backends. Other backends provide the savepoint +functions, but they're empty operations -- they don't actually do anything. Savepoints aren't especially useful if you are using the default ``autocommit`` behavior of Django. However, if you are using @@ -314,6 +314,21 @@ The following example demonstrates the use of savepoints:: Database-specific notes ======================= +Savepoints in SQLite +-------------------- + +While SQLite ≥ 3.6.8 supports savepoints, a flaw in the design of the +:mod:`sqlite3` makes them hardly usable. + +When autocommit is enabled, savepoints don't make sense. When it's disabled, +:mod:`sqlite3` commits implicitly before savepoint-related statement. (It +commits before any statement other than ``SELECT``, ``INSERT``, ``UPDATE``, +``DELETE`` and ``REPLACE``.) + +As a consequence, savepoints are only usable if you start a transaction +manually while in autocommit mode, and Django doesn't provide an API to +achieve that. + Transactions in MySQL --------------------- @@ -363,11 +378,11 @@ itself. Savepoint rollback ~~~~~~~~~~~~~~~~~~ -If you are using PostgreSQL 8 or later, you can use :ref:`savepoints -` to control the extent of a rollback. -Before performing a database operation that could fail, you can set or update -the savepoint; that way, if the operation fails, you can roll back the single -offending operation, rather than the entire transaction. For example:: +You can use :ref:`savepoints ` to control +the extent of a rollback. Before performing a database operation that could +fail, you can set or update the savepoint; that way, if the operation fails, +you can roll back the single offending operation, rather than the entire +transaction. For example:: a.save() # Succeeds, and never undone by savepoint rollback try: diff --git a/tests/transactions_regress/tests.py b/tests/transactions_regress/tests.py index 3d571fba2f1..e86db4d0aaa 100644 --- a/tests/transactions_regress/tests.py +++ b/tests/transactions_regress/tests.py @@ -309,6 +309,8 @@ class TestManyToManyAddTransaction(TransactionTestCase): class SavepointTest(TransactionTestCase): + @skipIf(connection.vendor == 'sqlite', + "SQLite doesn't support savepoints in managed mode") @skipUnlessDBFeature('uses_savepoints') def test_savepoint_commit(self): @commit_manually @@ -324,6 +326,8 @@ class SavepointTest(TransactionTestCase): work() + @skipIf(connection.vendor == 'sqlite', + "SQLite doesn't support savepoints in managed mode") @skipIf(connection.vendor == 'mysql' and connection.features._mysql_storage_engine == 'MyISAM', "MyISAM MySQL storage engine doesn't support savepoints")