Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Nested Transactions for System.Data.SQLite with Savepoints

5.00/5 (2 votes)
10 Sep 2016CPOL3 min read 15.5K   400  
A set of extension methods giving System.Data.SQLite access to Savepoints

Introduction

SQLite does not inherently allow nested transactions. However, it does allow for a transaction to be marked with intermediate savepoints, which emulate nested transactions to a degree. The documentation explaining how all this works can be found here^.

Background

Although SAVEPOINT commands can be issued directly on a database connection without a current transaction, doing so implicitly creates a transaction and alters the way the RELEASE command works. In order to alleviate any confusion, the module is set up so that Savepoints can only be created within the context of a transaction. The resulting updates to the database are therefore only written when a COMMIT is issued, and the entire transaction is aborted if a ROLLBACK is issued, regardless of any Releases or Rollbacks of Savepoints within the transaction. This is consistent with the way transactions work in general (with or without Savepoints).

Using the Code

All of the code required is in a single .cs file. Rather than putting it into a class library, just include the file into your projects and start using it (although you may build it into a separate library if you so desire). In fact, the code all exists in a single static extension class. There are no special objects as far as usage of the extension is concerned.

Most of the manipulation of Savepoints is handled in extension methods on the SQLiteTransaction object. There are four extension methods defined for the SQLiteTransaction object. There are also two BeginTransaction extension methods for the SQLiteConnection object, that take an additional string savepointName parameter, and automatically add the named Savepoint to the newly created Transaction.

It is important to note that all Savepoint names are case-insensitive, and do not have to be unique. Any Release or Rollback of a Savepoint will act upon the most recent one with a matching name.

The four extension methods defined for the SQLiteTransaction object are:

  • void AddSavepoint(string savepointName)

    This method, as its name suggests, creates a new Savepoint within the Transaction.

  • void ReleaseSavepoint(string savepointName)

    I think the best description of how this works is the 2nd definition in the SQLite documentation:

    Another view of RELEASE is that it merges a named transaction into its parent transaction, so that the named transaction and its parent become the same transaction. After RELEASE, the named transaction and its parent will commit or rollback together, whatever their fate may be.

    This means that all commands within the Savepoint that have not been previously rolled back are effectively appended to the commands immediately preceding the Savepoint, and the released Savepoint no longer exists in the transaction. Any Savepoints following the one being released, that have not been previously been released or rolled back, are also automatically released at this time.

  • void RollbackToSavepoint(string savepointName)

    This method rolls back all commands issued following the named Savepoint, even if those commands include Savepoints that have previously been Released. The named Savepoint itself, however, still exists. Any commands issued to the transaction after the Rollback are still considered to be a part of that Savepoint.

  • void RollbackAndRelease(string savepointName)

    This command encapsulates both a Rollback and a Release, issued in succession. When issued like this, the transaction is returned to the state it was in immediately before the named Savepoint was created.

Points of Interest

Performing a SQLiteTransaction.Commit(), SQLiteTransaction.Rollback(), or closing or disposing the underlying connection, automatically releases the list of Savepoints from the transaction. This is internally managed by attaching event handlers on the connection's Commit, Rollback, StateChange, and Disposed events. I did find out the hard way, however, that it was important to remove these event handlers from the list when disposing of the list. Because the events were attached to connection object, a new transaction created on that connection was firing the events within deleted lists.

I also learnt that Stack enumerators go from last to first item (i.e. the order the items will be removed, and not the order in which they were entered).

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)