OK, giving that a shot, and came up with a couple problems:
- Since I can't access SQLiteConnection._transactionLevel, I can't duplicate any of the state/sanity checks based on that
- Same problem with sanity checks based on SQLiteConnection._version
- I can't access the SQLiteConnection._sql member or its type to get the SQLiteBase.AutoCommit property for that sanity check
I think I've worked around #1 and #2 with judicious use of attaching event handlers to the connection. Short of evil reflection hacks, however, I don't see any way to work around #3.
For anyone else who wants to try this out, here's the code I'm using. AFAICT, it works fine, but caveat emptor: I've only tested it under normal, correct operations, I've not tested its resiliency in the face of misuse or other errors. In case the comment is not obvious, I officially release this code into the Public Domain, same as the rest of SQLite :)
using System;
using System.Data;
using System.Data.Common;
using System.Data.SQLite;
using System.Threading;
using InterfaceLib;
using System.Diagnostics.CodeAnalysis;
/********************************************************
* ADO.NET 2.0 Data Provider for SQLite Version 3.X
* Written by Robert Simpson (robert@blackcastlesoft.com)
* Adapted by Matthew Gabeler-Lee (matt.gabeler-lee@virgininstruments.com)
*
* Released to the public domain, use at your own risk!
********************************************************/
namespace UtilLib.SQLite
{
/// <summary>
/// Hacked up version of SQLiteTransaction that uses BEGIN EXCLUSIVE
/// </summary>
public sealed class SQLiteExclusiveTransaction : DbTransaction
{
/// <summary>
/// The connection to which this transaction is bound
/// </summary>
private SQLiteConnection connection;
/// <summary>
/// Is this transaction still open?
/// </summary>
private bool isOpen;
/// <summary>
/// Constructs the transaction object, binding it to the supplied connection
/// </summary>
/// <param name="connection">The connection to open a transaction on</param>
internal SQLiteExclusiveTransaction(SQLiteConnection connection)
{
this.connection = connection;
try
{
using (SQLiteCommand cmd = connection.CreateCommand())
{
cmd.CommandText = "BEGIN EXCLUSIVE";
cmd.ExecuteNonQuery();
}
}
catch (SQLiteException)
{
connection = null;
throw;
}
isOpen = true;
connection.StateChange += connection_StateChange;
connection.RollBack += connection_RollBack;
connection.Commit += connection_Commit;
}
private void connection_Commit(object sender, CommitEventArgs e)
{
OnTransactionDone();
}
private void connection_RollBack(object sender, EventArgs e)
{
OnTransactionDone();
}
private void connection_StateChange(object sender, StateChangeEventArgs e)
{
if (e.CurrentState == ConnectionState.Closed || e.CurrentState == ConnectionState.Broken)
OnTransactionDone();
}
private void OnTransactionDone()
{
if (!isOpen)
throw new AssertionFailedException();
isOpen = false;
connection.StateChange -= connection_StateChange;
connection.RollBack -= connection_RollBack;
connection.Commit -= connection_Commit;
}
/// <summary>
/// Commits the current transaction.
/// </summary>
public override void Commit()
{
IsValid(true);
if (isOpen)
{
using (SQLiteCommand cmd = connection.CreateCommand())
{
cmd.CommandText = "COMMIT";
cmd.ExecuteNonQuery();
}
}
// event handlers on the connection should have updated the isOpen flag by now
if (isOpen)
throw new AssertionFailedException();
connection = null;
}
/// <summary>
/// Returns the underlying connection to which this transaction applies.
/// </summary>
public new SQLiteConnection Connection
{
get
{
return connection;
}
}
/// <summary>
/// Forwards to the local Connection property
/// </summary>
protected override DbConnection DbConnection
{
get
{
return Connection;
}
}
/// <summary>
/// Disposes the transaction. If it is currently active, any changes are rolled back.
/// </summary>
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (IsValid(false))
{
IssueRollback();
}
}
base.Dispose(disposing);
}
/// <summary>
/// Gets the isolation level of the transaction. SQLite only supports Serializable transactions.
/// </summary>
public override IsolationLevel IsolationLevel
{
get
{
return IsolationLevel.Serializable;
}
}
/// <summary>
/// Rolls back the active transaction.
/// </summary>
public override void Rollback()
{
IsValid(true);
IssueRollback();
}
internal void IssueRollback()
{
SQLiteConnection cnn = Interlocked.Exchange(ref connection, null);
if (cnn != null)
{
using (SQLiteCommand cmd = cnn.CreateCommand())
{
cmd.CommandText = "ROLLBACK";
cmd.ExecuteNonQuery();
}
}
}
[SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "Matching behavior of existing SQLite code")]
internal bool IsValid(bool throwError)
{
if (connection == null)
{
if (throwError == true)
throw new ArgumentNullException("No connection associated with this transaction");
else
return false;
}
// we can't access the _version member
// the StateChange event handler should deal with this
#if CANTACCESSVERSION
if (connection._version != _version)
{
if (throwError == true)
throw new SQLiteException((int)SQLiteErrorCode.Misuse, "The connection was closed and re-opened, changes were already rolled back");
else
return false;
}
#endif
if (connection.State != ConnectionState.Open)
{
if (throwError == true)
throw new SQLiteException((int)SQLiteErrorCode.Misuse, "Connection was closed");
else
return false;
}
// want:
// || connection._sql.AutoCommit == true
if (!isOpen)
{
if (throwError == true)
throw new SQLiteException((int)SQLiteErrorCode.Misuse, "No transaction is active on this connection");
else
return false;
}
return true;
}
}
}