I'm trying to port some code to Python that uses sqlite databases, and I'm trying to get transactions to work, and I'm getting really confused. I'm really confused by this; I've used sqlite a lot in other languages, because it's great, but I simply cannot work out what's wrong here.
Here is the schema for my test database (to be fed into the sqlite3 command line tool).
BEGIN TRANSACTION;
CREATE TABLE test (i integer);
INSERT INTO "test" VALUES(99);
COMMIT;
Here is a test program.
import sqlite3
sql = sqlite3.connect("test.db")
with sql:
c = sql.cursor()
c.executescript("""
update test set i = 1;
fnord;
update test set i = 0;
""")
You may notice the deliberate mistake in it. This causes the SQL script to fail on the second line, after the update has been executed.
According to the docs, the with sql
statement is supposed to set up an implicit transaction around the contents, which is only committed if the block succeeds. However, when I run it, I get the expected SQL error... but the value of i is set from 99 to 1. I'm expecting it to remain at 99, because that first update should be rolled back.
Here is another test program, which explicitly calls commit()
and rollback()
.
import sqlite3
sql = sqlite3.connect("test.db")
try:
c = sql.cursor()
c.executescript("""
update test set i = 1;
fnord;
update test set i = 0;
""")
sql.commit()
except sql.Error:
print("failed!")
sql.rollback()
This behaves in precisely the same way --- i gets changed from 99 to 1.
Now I'm calling BEGIN and COMMIT explicitly:
import sqlite3
sql = sqlite3.connect("test.db")
try:
c = sql.cursor()
c.execute("begin")
c.executescript("""
update test set i = 1;
fnord;
update test set i = 0;
""")
c.execute("commit")
except sql.Error:
print("failed!")
c.execute("rollback")
This fails too, but in a different way. I get this:
sqlite3.OperationalError: cannot rollback - no transaction is active
However, if I replace the calls to c.execute()
to c.executescript()
, then it works (i remains at 99)!
(I should also add that if I put the begin
and commit
inside the inner call to executescript
then it behaves correctly in all cases, but unfortunately I can't use that approach in my application. In addition, changing sql.isolation_level
appears to make no difference to the behaviour.)
Can someone explain to me what's happening here? I need to understand this; if I can't trust the transactions in the database, I can't make my application work...
Python 2.7, python-sqlite3 2.6.0, sqlite3 3.7.13, Debian.
Normal
.execute()
's work as expected with the comfortable default auto-commit mode and thewith conn: ...
context manager doing auto-commit OR rollback - except for protected read-modify-write transactions, which are explained at the end of this answer.sqlite3 module's non-standard
conn_or_cursor.executescript()
doesn't take part in the (default) auto-commit mode (and so doesn't work normally with thewith conn: ...
context manager) but forwards the script rather raw. Therefor it just commits a potentially pending auto-commit transactions at start, before "going raw".This also means that without a "BEGIN" inside the script
executescript()
works without a transaction, and thus no rollback option upon error or otherwise.So with
executescript()
we better use a explicit BEGIN (just as your inital schema creation script did for the "raw" mode sqlite command line tool). And this interaction shows step by step whats going on:The script didn't reach the "COMMIT". And thus we could the view the current intermediate state and decide for rollback (or commit nevertheless)
Thus a working try-except-rollback via
excecutescript()
looks like this:(Note the rollback via script here, because no
.execute()
took over commit control)And here a note on the auto-commit mode in combination with the more difficult issue of a protected read-modify-write transaction - which made @Jeremie say "Out of all the many, many things written about transactions in sqlite/python, this is the only thing that let me do what I want (have an exclusive read lock on the database)." in a comment on an example which included a
c.execute("begin")
. Though sqlite3 normally does not make a long blocking exclusive read lock except for the duration of the actual write-back, but more clever 5-stage locks to achieve enough protection against overlapping changes.The
with conn:
auto-commit context does not already put or trigger a lock strong enough for protected read-modify-write in the 5-stage locking scheme of sqlite3. Such lock is made implicitely only when the first data-modifying command is issued - thus too late. Only an explicitBEGIN (DEFERRED) (TRANSACTION)
triggers the wanted behavior:So a protected read-modify-write transaction which uses the programming language in general way (and not a special atomic SQL UPDATE clause) looks like this:
Upon failure such read-modify-write transaction could be retried a couple of times.
Here's what I think is happening based on my reading of Python's sqlite3 bindings as well as official Sqlite3 docs. The short answer is that if you want a proper transaction, you should stick to this idiom:
Contrary to my intuition,
with connection
does not callBEGIN
upon entering the scope. In fact it doesn't do anything at all in__enter__
. It only has an effect when you__exit__
the scope, choosing eitherCOMMIT
orROLLBACK
depending on whether the scope is exiting normally or with an exception.Therefore, the right thing to do is to always explicitly mark the beginning of your transactions using
BEGIN
. This rendersisolation_level
irrelevant within transactions, because thankfully it only has an effect while autocommit mode is enabled, and autocommit mode is always suppressed within transaction blocks.Another quirk is
executescript
, which always issues aCOMMIT
before running your script. This can easily mess up the transactions, so your choice is to eitherexecutescript
within a transaction and nothing else, orexecutescript
entirely; you can callexecute
as many times as you want, subject to the one-statement-per-execute
limitation.Python's DB API tries to be smart, and begins and commits transactions automatically.
I would recommend to use a DB driver that does not use the Python DB API, like apsw.
For anyone who'd like to work with the sqlite3 lib regardless of its shortcomings, I found that you can keep some control of transactions if you do these two things:
Connection.isolation_level = None
(as per the docs, this means autocommit mode)executescript
at all, because according to the docs it "issues a COMMIT statement first" - ie, trouble. Indeed I found it interferes with any manually set transactionsSo then, the following adaptation of your test works for me:
Per the docs,
Therefore, if you let Python exit the with-statement when an exception occurs, the transaction will be rolled back.
yields
as expected.
This is a bit old thread but if it helps I've found that doing a rollback on the connection object does the trick.