Browse Source

Added support for savepoints in SQLite.

Technically speaking they aren't usable yet.
Aymeric Augustin 12 years ago
parent
commit
4b31a6a9e6

+ 10 - 0
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

+ 1 - 2
docs/ref/databases.txt

@@ -424,8 +424,7 @@ Savepoints
 
 Both the Django ORM and MySQL (when using the InnoDB :ref:`storage engine
 <mysql-storage-engines>`) support database :ref:`savepoints
-<topics-db-transactions-savepoints>`, but this feature wasn't available in
-Django until version 1.4 when such support was added.
+<topics-db-transactions-savepoints>`.
 
 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

+ 25 - 10
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
-<topics-db-transactions-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 <topics-db-transactions-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:

+ 4 - 0
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")