Skip to content

Commit

Permalink
Microsoft.Data.Sqlite: Add deferred transactions (#21212)
Browse files Browse the repository at this point in the history
* Add deferred transactions.

* Make new SqliteConnection.BeginTransaction overloads virtual

Co-authored-by: Brice Lambson <brice@bricelam.net>
  • Loading branch information
AlexanderTaeschner and bricelam committed Aug 11, 2020
1 parent 05668ce commit 9f9e239
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 4 deletions.
39 changes: 38 additions & 1 deletion src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,24 @@ public virtual void CreateCollation<T>(string name, T state, Func<T, string, str
public new virtual SqliteTransaction BeginTransaction()
=> BeginTransaction(IsolationLevel.Unspecified);

/// <summary>
/// Begins a transaction on the connection.
/// </summary>
/// <param name="deferred"><see langword="true"/> to defer the creation of the transaction.
/// This also causes transactions to upgrade from read transactions to write transactions as needed by their commands.</param>
/// <returns>The transaction.</returns>
/// <remarks>
/// Warning, commands inside a deferred transaction can fail if they cause the
/// transaction to be upgraded from a read transaction to a write transaction
/// but the database is locked. The application will need to retry the entire
/// transaction when this happens.
/// </remarks>
/// <exception cref="SqliteException">A SQLite error occurs during execution.</exception>
/// <seealso href="https://docs.microsoft.com/dotnet/standard/data/sqlite/transactions">Transactions</seealso>
/// <seealso href="https://docs.microsoft.com/dotnet/standard/data/sqlite/database-errors">Database Errors</seealso>
public virtual SqliteTransaction BeginTransaction(bool deferred)
=> BeginTransaction(IsolationLevel.Unspecified, deferred);

/// <summary>
/// Begins a transaction on the connection.
/// </summary>
Expand All @@ -490,6 +508,25 @@ protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLeve
/// <seealso href="https://docs.microsoft.com/dotnet/standard/data/sqlite/transactions">Transactions</seealso>
/// <seealso href="https://docs.microsoft.com/dotnet/standard/data/sqlite/database-errors">Database Errors</seealso>
public new virtual SqliteTransaction BeginTransaction(IsolationLevel isolationLevel)
=> BeginTransaction(isolationLevel, deferred: isolationLevel == IsolationLevel.ReadUncommitted);

/// <summary>
/// Begins a transaction on the connection.
/// </summary>
/// <param name="isolationLevel">The isolation level of the transaction.</param>
/// <param name="deferred"><see langword="true"/> to defer the creation of the transaction.
/// This also causes transactions to upgrade from read transactions to write transactions as needed by their commands.</param>
/// <returns>The transaction.</returns>
/// <remarks>
/// Warning, commands inside a deferred transaction can fail if they cause the
/// transaction to be upgraded from a read transaction to a write transaction
/// but the database is locked. The application will need to retry the entire
/// transaction when this happens.
/// </remarks>
/// <exception cref="SqliteException">A SQLite error occurs during execution.</exception>
/// <seealso href="https://docs.microsoft.com/dotnet/standard/data/sqlite/transactions">Transactions</seealso>
/// <seealso href="https://docs.microsoft.com/dotnet/standard/data/sqlite/database-errors">Database Errors</seealso>
public virtual SqliteTransaction BeginTransaction(IsolationLevel isolationLevel, bool deferred)
{
if (State != ConnectionState.Open)
{
Expand All @@ -501,7 +538,7 @@ protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLeve
throw new InvalidOperationException(Resources.ParallelTransactionsNotSupported);
}

return Transaction = new SqliteTransaction(this, isolationLevel);
return Transaction = new SqliteTransaction(this, isolationLevel, deferred);
}

/// <summary>
Expand Down
6 changes: 3 additions & 3 deletions src/Microsoft.Data.Sqlite.Core/SqliteTransaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ public class SqliteTransaction : DbTransaction
private readonly IsolationLevel _isolationLevel;
private bool _completed;

internal SqliteTransaction(SqliteConnection connection, IsolationLevel isolationLevel)
internal SqliteTransaction(SqliteConnection connection, IsolationLevel isolationLevel, bool deferred)
{
if ((isolationLevel == IsolationLevel.ReadUncommitted
&& connection.ConnectionOptions.Cache != SqliteCacheMode.Shared)
&& ((connection.ConnectionOptions.Cache != SqliteCacheMode.Shared) || !deferred))
|| isolationLevel == IsolationLevel.ReadCommitted
|| isolationLevel == IsolationLevel.RepeatableRead)
{
Expand All @@ -46,7 +46,7 @@ internal SqliteTransaction(SqliteConnection connection, IsolationLevel isolation
}

connection.ExecuteNonQuery(
IsolationLevel == IsolationLevel.Serializable
IsolationLevel == IsolationLevel.Serializable && !deferred
? "BEGIN IMMEDIATE;"
: "BEGIN;");
sqlite3_rollback_hook(connection.Handle, RollbackExternal, null);
Expand Down
37 changes: 37 additions & 0 deletions test/Microsoft.Data.Sqlite.Tests/SqliteTransactionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Data;
using System.IO;
using Microsoft.Data.Sqlite.Properties;
using Xunit;
using static SQLitePCL.raw;
Expand Down Expand Up @@ -113,6 +114,42 @@ public void Serialized_disallows_dirty_reads()
}
}

[Fact]
public void Deferred_allows_parallel_reads()
{
const string connectionString = "Data Source=deferred.db";

try
{
using (var connection = new SqliteConnection(connectionString))
{
connection.Open();
connection.ExecuteNonQuery("CREATE TABLE Data (Value); INSERT INTO Data VALUES (42);");
}

using (var connection1 = new SqliteConnection(connectionString))
using (var connection2 = new SqliteConnection(connectionString))
{
connection1.Open();
connection2.Open();

using (connection1.BeginTransaction(deferred: true))
using (connection2.BeginTransaction(deferred: true))
{
var value1 = connection1.ExecuteScalar<long>("SELECT * FROM Data;");
var value2 = connection2.ExecuteScalar<long>("SELECT * FROM Data;");

Assert.Equal(42, value1);
Assert.Equal(42, value2);
}
}
}
finally
{
File.Delete("deferred.db");
}
}

[Fact]
public void IsolationLevel_throws_when_completed()
{
Expand Down

0 comments on commit 9f9e239

Please sign in to comment.