Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Microsoft.Data.Sqlite: Add deferred transactions #21212

Merged
2 commits merged into from
Aug 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
AlexanderTaeschner marked this conversation as resolved.
Show resolved Hide resolved
/// <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