From 9f9e239c2cab1386dd744a8bd4532c5e421e9f83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20T=C3=A4schner?= Date: Tue, 11 Aug 2020 04:15:49 +0200 Subject: [PATCH] Microsoft.Data.Sqlite: Add deferred transactions (#21212) * Add deferred transactions. * Make new SqliteConnection.BeginTransaction overloads virtual Co-authored-by: Brice Lambson --- .../SqliteConnection.cs | 39 ++++++++++++++++++- .../SqliteTransaction.cs | 6 +-- .../SqliteTransactionTest.cs | 37 ++++++++++++++++++ 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs b/src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs index 84875144d86..3497151c7e9 100644 --- a/src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs +++ b/src/Microsoft.Data.Sqlite.Core/SqliteConnection.cs @@ -473,6 +473,24 @@ public virtual void CreateCollation(string name, T state, Func BeginTransaction(IsolationLevel.Unspecified); + /// + /// Begins a transaction on the connection. + /// + /// to defer the creation of the transaction. + /// This also causes transactions to upgrade from read transactions to write transactions as needed by their commands. + /// The transaction. + /// + /// 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. + /// + /// A SQLite error occurs during execution. + /// Transactions + /// Database Errors + public virtual SqliteTransaction BeginTransaction(bool deferred) + => BeginTransaction(IsolationLevel.Unspecified, deferred); + /// /// Begins a transaction on the connection. /// @@ -490,6 +508,25 @@ protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLeve /// Transactions /// Database Errors public new virtual SqliteTransaction BeginTransaction(IsolationLevel isolationLevel) + => BeginTransaction(isolationLevel, deferred: isolationLevel == IsolationLevel.ReadUncommitted); + + /// + /// Begins a transaction on the connection. + /// + /// The isolation level of the transaction. + /// to defer the creation of the transaction. + /// This also causes transactions to upgrade from read transactions to write transactions as needed by their commands. + /// The transaction. + /// + /// 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. + /// + /// A SQLite error occurs during execution. + /// Transactions + /// Database Errors + public virtual SqliteTransaction BeginTransaction(IsolationLevel isolationLevel, bool deferred) { if (State != ConnectionState.Open) { @@ -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); } /// diff --git a/src/Microsoft.Data.Sqlite.Core/SqliteTransaction.cs b/src/Microsoft.Data.Sqlite.Core/SqliteTransaction.cs index 4ff1439fd65..3c563add19a 100644 --- a/src/Microsoft.Data.Sqlite.Core/SqliteTransaction.cs +++ b/src/Microsoft.Data.Sqlite.Core/SqliteTransaction.cs @@ -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) { @@ -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); diff --git a/test/Microsoft.Data.Sqlite.Tests/SqliteTransactionTest.cs b/test/Microsoft.Data.Sqlite.Tests/SqliteTransactionTest.cs index 9cb0ede916c..73f9ea6d076 100644 --- a/test/Microsoft.Data.Sqlite.Tests/SqliteTransactionTest.cs +++ b/test/Microsoft.Data.Sqlite.Tests/SqliteTransactionTest.cs @@ -3,6 +3,7 @@ using System; using System.Data; +using System.IO; using Microsoft.Data.Sqlite.Properties; using Xunit; using static SQLitePCL.raw; @@ -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("SELECT * FROM Data;"); + var value2 = connection2.ExecuteScalar("SELECT * FROM Data;"); + + Assert.Equal(42, value1); + Assert.Equal(42, value2); + } + } + } + finally + { + File.Delete("deferred.db"); + } + } + [Fact] public void IsolationLevel_throws_when_completed() {