8

I'm writing an application in Python using SQLAlchemy (and Elixir) with SQLite as the database backend. I start a new transaction using the code session.begin_transaction(), but when I call session.rollback() I get the following error:

sqlalchemy.exceptions.OperationalError: (OperationalError) no such savepoint: sa_savepoint_1 u'ROLLBACK TO SAVEPOINT sa_savepoint_1' []

I also get a similar error calling session.commit(). From what I can tell, sqlite supports SAVEPOINTS (http://www.sqlite.org/lang_savepoint.html).

How do I get nested transactions to work?

Ben
  • 51,770
  • 36
  • 127
  • 149
Jon
  • 9,815
  • 9
  • 46
  • 67
  • I could more easily answer this if you create a demonstration snippet of code. You can do so easily using the sqlite :memory: database, which creates an in-memory DB. – Ken Kinder Nov 09 '09 at 03:19
  • Related question: http://stackoverflow.com/questions/2036378/using-savepoints-in-python-sqlite3 – Shane Holloway Jan 11 '10 at 16:24

3 Answers3

11

I've run into this issue using nested transactions, using Python 3 on Windows. I'm using SQLite version 3.8.11, so SAVEPOINT should be supported. Apparently installing pysqlite isn't an option for me as it doesn't support Python 3.

After hours of banging my head against the desk I came across this section in the documentation:

http://docs.sqlalchemy.org/en/latest/dialects/sqlite.html#serializable-isolation-savepoints-transactional-ddl

In the section Database Locking Behavior / Concurrency, we refer to the pysqlite driver’s assortment of issues that prevent several features of SQLite from working correctly. The pysqlite DBAPI driver has several long-standing bugs which impact the correctness of its transactional behavior. In its default mode of operation, SQLite features such as SERIALIZABLE isolation, transactional DDL, and SAVEPOINT support are non-functional, and in order to use these features, workarounds must be taken.

The issue is essentially that the driver attempts to second-guess the user’s intent, failing to start transactions and sometimes ending them prematurely, in an effort to minimize the SQLite databases’s file locking behavior, even though SQLite itself uses “shared” locks for read-only activities.

SQLAlchemy chooses to not alter this behavior by default, as it is the long-expected behavior of the pysqlite driver; if and when the pysqlite driver attempts to repair these issues, that will be more of a driver towards defaults for SQLAlchemy.

The good news is that with a few events, we can implement transactional support fully, by disabling pysqlite’s feature entirely and emitting BEGIN ourselves. This is achieved using two event listeners:

from sqlalchemy import create_engine, event

engine = create_engine("sqlite:///myfile.db")

@event.listens_for(engine, "connect")
def do_connect(dbapi_connection, connection_record):
    # disable pysqlite's emitting of the BEGIN statement entirely.
    # also stops it from emitting COMMIT before any DDL.
    dbapi_connection.isolation_level = None

@event.listens_for(engine, "begin")
def do_begin(conn):
    # emit our own BEGIN
    conn.execute("BEGIN")

Adding the listeners above completely resolved the issue for me!

I've published a full working example as a gist:

https://gist.github.com/snorfalorpagus/c48770e7d1fcb9438830304c4cca24b9

I also found logging the SQL statements helpful (this is used in the above example):

Debugging (displaying) SQL command sent to the db by SQLAlchemy

Snorfalorpagus
  • 3,348
  • 2
  • 29
  • 51
3

Although sqlite does appear to support nested transactions via SAVEPOINT, it's only as of version 3.6.8, released 2009 Jan 12. Python, at least up to v2.6, uses earlier versions:

c:\svn\core\apps\general>python
Python 2.6.2 (r262:71605, Apr 14 2009, 22:40:02) [MSC v.1500 32 bit (Intel)] on win32
>>> import sqlite3 as s
>>> s.sqlite_version
'3.5.9'

I believe you can install PySqlite yourself and the latest appears to support v3.6.12. I can't say for sure this will solve your problem though, but I believe the answer explains why it's not working for you now.

Peter Hansen
  • 21,046
  • 5
  • 50
  • 72
  • sqlite3 version is not bound to python version – iny Jan 03 '10 at 11:25
  • @iny, not quite sure what your point is. Python's version of sqlite (i.e. the one included with Python) is of course bound to a particular version of sqlite. The other way around... well I certainly don't say that above. – Peter Hansen Jan 03 '10 at 15:38
  • 1
    Appears Python 2.7 also ships with an old SQLite3 library. Installing PySqlite solved this for me. – Kiran Jonnalagadda Nov 17 '15 at 20:36
  • @KiranJonnalagadda although Python 2.7 may not have the most recent version, doesn't it have a version greater than 3.6.8? On Ubuntu 12.04 with Python 2.7.3 I get sqlite v3.7.9, and on Windows with Python 2.7.5 I get sqlite v3.6.21. Are you trying Python 2.7.2 or earlier? – Peter Hansen Nov 18 '15 at 20:47
  • Freshly installed Python 2.7 via Homebrew on Mac OS X (not system installed Python) had this problem. PySqlite fixed it. – Kiran Jonnalagadda Nov 20 '15 at 07:15
  • @KiranJonnalagadda Any chance you can check *which* version of Python 2.7 that was, if it wasn't the first? It will show at the prompt if you run python at the command line. They've gone through 2.7.0 (which is what the first 2.7 was in effect) through 2.7.10. – Peter Hansen Nov 20 '15 at 17:02
  • 1
    @PeterHansen I'm on 2.7.8. – Kiran Jonnalagadda Nov 21 '15 at 18:06
0

SQLAlchemy uses pysqlite to interact with an SQLite database, if I'm not mistaken pysqlite will by default wrap up any query you send it in a transaction.

The answer might lie in correctly setting the isolation level when you connect.

Some discussion about that over here

Dirk Stoop
  • 3,080
  • 21
  • 18
  • I want nested transactions, where you can rollback without losing anything that has changed in the outer transaction. I'm not sure that pysqlite can do that automatically. Are you suggesting that using SAVEPOINTS are not possible, or just suggesting another (possible) way to solve the problem? – Jon Oct 31 '09 at 17:16