Skip to content

Commit 319a05a

Browse files
committed
Add clone, and clone db. Fix behavior of *All methods with transactions.
1 parent a1ed74c commit 319a05a

4 files changed

Lines changed: 91 additions & 83 deletions

File tree

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -194,12 +194,13 @@ if (String.IsNullOrWhiteSpace(cipherVer))
194194

195195
## Using transactions
196196

197-
Warning: all transactions methods create a state in this connection (the transaction depth).
198-
Be sure to not share the connection with other simultaneous threads.
197+
Warning: all transactions methods create a state in this connection (the transaction depth).
198+
Be sure to not share the connection with other simultaneous threads.
199+
You can use `using var tempConnection = connection.Clone()` to prevent this issue.
199200

200-
Especially these methods create by default an implicit transaction:
201-
`InsertAll(), InsertOrUpdateAll(), ReplaceAll()`
202-
They all have a boolean parameter to disable the implicit transaction (beware of performances).
201+
The following methods use `Clone` to clone the connection and prevent any interaction of the transaction they create with your code:
202+
`InsertAll(), InsertOrUpdateAll(), ReplaceAll()`
203+
They all have a boolean parameter to disable this behavior (beware of performances), which will also prevent a correct rollback in case an exception occurs.
203204

204205
Standard transaction:
205206
```c#

SQLite.Net.Tests/InsertTest.cs

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -115,31 +115,36 @@ public void InsertALot()
115115
[Test]
116116
public void InsertAllFailureInsideTransaction()
117117
{
118-
List<UniqueObj> testObjects = Enumerable.Range(1, 20).Select(i => new UniqueObj
119-
{
120-
Id = i
121-
}).ToList();
122-
testObjects[testObjects.Count - 1].Id = 1; // causes the insert to fail because of duplicate key
123-
124-
ExceptionAssert.Throws<SQLiteException>(() => _db.RunInTransaction(() => { _db.InsertAll(testObjects); }));
118+
var testObjects = Enumerable.Range(1, 20).Select(i => new UniqueObj { Id = i }).ToList();
119+
testObjects[^1].Id = 1; // causes the insert to fail because of duplicate key
125120

121+
ExceptionAssert.Throws<SQLiteException>(() => _db.RunInTransaction(() => { _db.InsertAll(testObjects, false); }));
126122
Assert.AreEqual(0, _db.Table<UniqueObj>().Count());
127123
}
128124

129125
[Test]
130126
public void InsertAllFailureOutsideTransaction()
131127
{
132-
List<UniqueObj> testObjects = Enumerable.Range(1, 20).Select(i => new UniqueObj
133-
{
134-
Id = i
135-
}).ToList();
136-
testObjects[testObjects.Count - 1].Id = 1; // causes the insert to fail because of duplicate key
128+
var testObjects = Enumerable.Range(1, 20).Select(i => new UniqueObj { Id = i }).ToList();
129+
testObjects[^1].Id = 1; // causes the insert to fail because of duplicate key
137130

138-
ExceptionAssert.Throws<SQLiteException>(() => _db.InsertAll(testObjects));
131+
ExceptionAssert.Throws<SQLiteException>(() => _db.InsertAll(testObjects, true));
139132

140133
Assert.AreEqual(0, _db.Table<UniqueObj>().Count());
141134
}
142135

136+
[Test]
137+
public void InsertAllFailureSucceedsOutsideTransaction()
138+
{
139+
_db.DeleteAll<UniqueObj>();
140+
var testObjects = Enumerable.Range(1, 20).Select(i => new UniqueObj { Id = i }).ToList();
141+
testObjects[^1].Id = 1; // causes the last insert to fail because of duplicate key, but will let all previous insert in the db
142+
143+
ExceptionAssert.Throws<SQLiteException>(() => _db.InsertAll(testObjects, false));
144+
145+
Assert.AreEqual(19, _db.Table<UniqueObj>().Count());
146+
}
147+
143148
[Test]
144149
public void InsertAllSuccessInsideTransaction()
145150
{

nuget/pack.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ if ($IsMacOS) {
77
$msbuild = join-path $msbuild 'MSBuild\Current\Bin\MSBuild.exe'
88
}
99
$version="2.1.0"
10-
$versionSuffix="-pre1"
10+
$versionSuffix="-pre2"
1111

1212
#####################
1313
#Build release config

src/SQLite.Net/SQLiteConnection.cs

Lines changed: 66 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ namespace SQLite.Net2
4545
public class SQLiteConnection : IDisposable
4646
{
4747
private static readonly IDbHandle? NullHandle = null;
48-
readonly SqliteApi sqlite = SqliteApi.Instance;
48+
private readonly SqliteApi sqlite = SqliteApi.Instance;
4949

5050
/// <summary>
5151
/// Used to list some code that we want the MonoTouch linker
@@ -56,17 +56,23 @@ public class SQLiteConnection : IDisposable
5656
#pragma warning restore 649
5757
private readonly Random _rand = new ();
5858
private readonly ConcurrentDictionary<string, TableMapping> _tableMappings;
59+
private readonly ConcurrentDictionary<(string MappedTypeFullName, string Extra), PreparedSqlLiteInsertCommand> _insertCommandMap = new ();
60+
61+
private IColumnInformationProvider? _columnInformationProvider;
5962
private TimeSpan _busyTimeout;
6063
private long _elapsedMilliseconds;
6164
private bool _open;
6265
private Stopwatch? _sw;
66+
private readonly SQLiteOpenFlags databaseOpenFlags;
6367

64-
public IBlobSerializer Serializer { get; }
68+
public IBlobSerializer? Serializer { get; }
6569
public string DatabasePath { get;}
6670
public bool StoreDateTimeAsTicks { get; }
6771
public IDictionary<Type, string> ExtraTypeMappings { get; }
6872
public IContractResolver Resolver { get; }
69-
73+
public IDbHandle Handle { get; private set; }
74+
public bool TimeExecution { get; set; }
75+
public ITraceListener? TraceListener { get; set; }
7076

7177
static SQLiteConnection()
7278
{
@@ -108,11 +114,20 @@ static SQLiteConnection()
108114
/// A contract resovler for resolving interfaces to concreate types during object creation
109115
/// </param>
110116

111-
public SQLiteConnection(string databasePath, bool storeDateTimeAsTicks = true, IBlobSerializer serializer = null, IDictionary<string, TableMapping> tableMappings = null, IDictionary<Type, string> extraTypeMappings = null, IContractResolver resolver = null)
117+
public SQLiteConnection(string databasePath, bool storeDateTimeAsTicks = true, IBlobSerializer? serializer = null, IDictionary<string, TableMapping>? tableMappings = null, IDictionary<Type, string>? extraTypeMappings = null, IContractResolver? resolver = null)
112118
: this(databasePath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create, storeDateTimeAsTicks, serializer, tableMappings, extraTypeMappings, resolver)
113119
{
114120
}
115121

122+
/// <summary>
123+
/// Create a new connection to the same database.
124+
/// </summary>
125+
/// <remarks>
126+
/// This support scenarios where a code needs to create a transaction, while leaving the current connection transactionless (ie: sharable with other codes), as a transaction creates a state in this object.
127+
/// </remarks>
128+
public SQLiteConnection Clone()
129+
=> new (DatabasePath, databaseOpenFlags, StoreDateTimeAsTicks, Serializer, _tableMappings, ExtraTypeMappings, Resolver);
130+
116131
/// <summary>
117132
/// Constructs a new SQLiteConnection and opens a SQLite database specified by databasePath.
118133
/// </summary>
@@ -143,60 +158,40 @@ public SQLiteConnection(string databasePath, bool storeDateTimeAsTicks = true,
143158
/// <param name="resolver">
144159
/// A contract resovler for resolving interfaces to concreate types during object creation
145160
/// </param>
146-
147161
public SQLiteConnection(string databasePath, SQLiteOpenFlags openFlags, bool storeDateTimeAsTicks = true, IBlobSerializer? serializer = null, IDictionary<string, TableMapping>? tableMappings = null,
148162
IDictionary<Type, string>? extraTypeMappings = null, IContractResolver? resolver = null)
149163
{
150-
ExtraTypeMappings = extraTypeMappings ?? new Dictionary<Type, string>();
151-
Serializer = serializer;
152-
Resolver = resolver ?? ContractResolver.Current;
153-
154-
_tableMappings = new (tableMappings ?? new Dictionary<string, TableMapping>());
155-
156164
if (string.IsNullOrEmpty(databasePath))
157-
{
158-
throw new ArgumentException("Must be specified", "databasePath");
159-
}
160-
165+
throw new ArgumentException("Must be specified", nameof(databasePath));
161166
DatabasePath = databasePath;
162167

163-
IDbHandle handle;
164-
var r = sqlite.Open(DatabasePath, out handle, (int) openFlags, null);
165-
166-
Handle = handle;
168+
var r = sqlite.Open(DatabasePath, out var handle, (int) openFlags, null);
167169
if (r != Result.OK)
168-
{
169-
throw new SQLiteException(r, string.Format("Could not open database file: {0} ({1})", DatabasePath, r));
170-
}
171-
172-
if (handle == null)
173-
{
174-
throw new NullReferenceException("Database handle is null");
175-
}
176-
177-
Handle = handle;
170+
throw new SQLiteException(r, $"Could not open database file: {DatabasePath} ({r})");
178171

172+
Handle = handle ?? throw new NullReferenceException("Database handle is null");
179173
_open = true;
180-
181-
StoreDateTimeAsTicks = storeDateTimeAsTicks;
174+
databaseOpenFlags = openFlags;
182175

183176
BusyTimeout = TimeSpan.FromSeconds(0.1);
177+
Serializer = serializer;
178+
StoreDateTimeAsTicks = storeDateTimeAsTicks;
179+
ExtraTypeMappings = extraTypeMappings ?? new Dictionary<Type, string>();
180+
Resolver = resolver ?? ContractResolver.Current;
181+
_tableMappings = new (tableMappings ?? new Dictionary<string, TableMapping>());
184182
}
185183

186-
private IColumnInformationProvider _columnInformationProvider;
187184
public IColumnInformationProvider ColumnInformationProvider
188185
{
189-
get { return _columnInformationProvider; }
190-
set
186+
get => _columnInformationProvider;
187+
set
191188
{
192189
_columnInformationProvider = value;
193190
Orm.ColumnInformationProvider = _columnInformationProvider ?? new DefaultColumnInformationProvider ();
194191
}
195192
}
196193

197-
public IDbHandle Handle { get; private set; }
198-
public bool TimeExecution { get; set; }
199-
public ITraceListener TraceListener { get; set; }
194+
200195

201196
/// <summary>
202197
/// Sets a busy handler to sleep the specified amount of time when a table is locked.
@@ -440,7 +435,8 @@ public int CreateIndex(string tableName, string columnName, bool unique = false)
440435
/// <param name="columnNames">An array of column names to index</param>
441436
/// <param name="unique">Whether the index should be unique</param>
442437

443-
public int CreateIndex(string tableName, string[] columnNames, bool unique = false) => CreateIndex(tableName + "_" + string.Join("_", columnNames), tableName, columnNames, unique);
438+
public int CreateIndex(string tableName, string[] columnNames, bool unique = false)
439+
=> CreateIndex(tableName + "_" + string.Join("_", columnNames), tableName, columnNames, unique);
444440

445441
/// <summary>
446442
/// Creates an index for the specified object property.
@@ -953,7 +949,7 @@ public string SaveTransactionPoint()
953949

954950
try
955951
{
956-
Execute("savepoint " + retVal);
952+
Execute($"savepoint {retVal}");
957953
}
958954
catch (Exception)
959955
{
@@ -1079,15 +1075,17 @@ private void DoSavePointExecute(string savePoint, string cmd)
10791075
/// </remarks>
10801076
public void RunInTransaction(Action action)
10811077
{
1082-
var savePoint = SaveTransactionPoint();
1078+
string? savePoint = null;
10831079
try
10841080
{
1081+
savePoint = SaveTransactionPoint();
10851082
action();
10861083
Release(savePoint);
10871084
}
1088-
catch (Exception)
1085+
catch (Exception e)
10891086
{
1090-
RollbackTo(savePoint, true);
1087+
if(savePoint != null)
1088+
RollbackTo(savePoint, true);
10911089
throw;
10921090
}
10931091
}
@@ -1110,10 +1108,11 @@ public int InsertAll(IEnumerable objects, bool runInTransaction = true)
11101108
var c = 0;
11111109
if (runInTransaction)
11121110
{
1113-
RunInTransaction(() =>
1111+
using var db = Clone();
1112+
db.RunInTransaction(() =>
11141113
{
11151114
foreach (var r in objects)
1116-
c += Insert(r);
1115+
c += db.Insert(r);
11171116
});
11181117
}
11191118
else
@@ -1145,10 +1144,11 @@ public int InsertAll(IEnumerable objects, string extra, bool runInTransaction =
11451144
var c = 0;
11461145
if (runInTransaction)
11471146
{
1148-
RunInTransaction(() =>
1147+
using var db = Clone();
1148+
db.RunInTransaction(() =>
11491149
{
11501150
foreach (var r in objects)
1151-
c += Insert(r, extra);
1151+
c += db.Insert(r, extra);
11521152
});
11531153
}
11541154
else
@@ -1180,10 +1180,11 @@ public int InsertAll(IEnumerable objects, Type objType, bool runInTransaction =
11801180
var c = 0;
11811181
if (runInTransaction)
11821182
{
1183-
RunInTransaction(() =>
1183+
using var db = Clone();
1184+
db.RunInTransaction(() =>
11841185
{
11851186
foreach (var r in objects)
1186-
c += Insert(r, objType);
1187+
c += db.Insert(r, objType);
11871188
});
11881189
}
11891190
else
@@ -1251,10 +1252,11 @@ public int InsertOrReplace(object? obj)
12511252
public int InsertOrReplaceAll(IEnumerable objects)
12521253
{
12531254
var c = 0;
1254-
RunInTransaction(() =>
1255+
using var db = Clone();
1256+
db.RunInTransaction(() =>
12551257
{
12561258
foreach (var r in objects)
1257-
c += InsertOrReplace(r);
1259+
c += db.InsertOrReplace(r);
12581260
});
12591261
return c;
12601262
}
@@ -1316,10 +1318,11 @@ public int InsertOrReplace(object obj, Type objType)
13161318
public int InsertOrReplaceAll(IEnumerable objects, Type objType)
13171319
{
13181320
var c = 0;
1319-
RunInTransaction(() =>
1321+
using var db = Clone();
1322+
db.RunInTransaction(() =>
13201323
{
13211324
foreach (var r in objects)
1322-
c += InsertOrReplace(r, objType);
1325+
c += db.InsertOrReplace(r, objType);
13231326
});
13241327
return c;
13251328
}
@@ -1382,20 +1385,20 @@ public int Insert(object? obj, string extra, Type? objType)
13821385
var insertCmd = GetInsertCommand(map, extra);
13831386
int count;
13841387

1385-
lock (insertCmd)
1388+
try
13861389
{
13871390
// We lock here to protect the prepared statement returned via GetInsertCommand.
13881391
// A SQLite prepared statement can be bound for only one operation at a time.
1389-
try
1392+
lock (insertCmd)
13901393
{
13911394
count = insertCmd.ExecuteNonQuery(vals);
13921395
}
1393-
catch (SQLiteException ex)
1394-
{
1395-
if (sqlite.ExtendedErrCode(Handle) == ExtendedResult.ConstraintNotNull)
1396-
throw new NotNullConstraintViolationException(ex.Result, ex.Message, map, obj);
1397-
throw;
1398-
}
1396+
}
1397+
catch (SQLiteException ex)
1398+
{
1399+
if (sqlite.ExtendedErrCode(Handle) == ExtendedResult.ConstraintNotNull)
1400+
throw new NotNullConstraintViolationException(ex.Result, ex.Message, map, obj);
1401+
throw;
13991402
}
14001403

14011404
if (map.HasAutoIncPK)
@@ -1410,8 +1413,6 @@ public int Insert(object? obj, string extra, Type? objType)
14101413
return count;
14111414
}
14121415

1413-
readonly ConcurrentDictionary<(string MappedTypeFullName, string Extra), PreparedSqlLiteInsertCommand> _insertCommandMap = new ();
1414-
14151416
private PreparedSqlLiteInsertCommand GetInsertCommand(TableMapping map, string extra)
14161417
{
14171418
var key = (map.MappedType.FullName, extra);
@@ -1567,10 +1568,11 @@ public int UpdateAll(IEnumerable objects, bool runInTransaction = true)
15671568
var c = 0;
15681569
if (runInTransaction)
15691570
{
1570-
RunInTransaction(() =>
1571+
using var db = Clone();
1572+
db.RunInTransaction(() =>
15711573
{
15721574
foreach (var r in objects)
1573-
c += Update(r);
1575+
c += db.Update(r);
15741576
});
15751577
}
15761578
else

0 commit comments

Comments
 (0)