Skip to content

Commit 4810afe

Browse files
committed
Refactor database configuration into a builder class and eliminate the need for expando's and other weird static state issues
1 parent 273c3aa commit 4810afe

14 files changed

Lines changed: 1426 additions & 1264 deletions

SQLitePCL.pretty.Async/AsyncDatabaseConnection.cs

Lines changed: 63 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -25,89 +25,71 @@ limitations under the License.
2525
using System.Runtime.CompilerServices;
2626
using System.Threading;
2727
using System.Threading.Tasks;
28+
using System.Reactive.Subjects;
2829

2930
namespace SQLitePCL.pretty
3031
{
31-
/// <summary>
32-
/// Extensions methods for <see cref="IDatabaseConnection"/>
33-
/// </summary>
34-
public static partial class DatabaseConnection
32+
internal class WriteLockedRef<T>
3533
{
36-
private const string AsyncDatabaseConnectionKey = "AsyncDatabaseConnection";
34+
private readonly object gate = new object();
35+
private T value;
3736

38-
// FIXME: I picked this number fairly randomly. It would be good to do some experimentation
39-
// to determine if its a good default. The goal should be supporting cancelling of queries that are
40-
// actually blocking use of the database for a measurable period of time.
41-
private static readonly int defaultInterruptInstructionCount = 100;
42-
43-
private static volatile IScheduler defaultScheduler = TaskPoolScheduler.Default;
37+
public WriteLockedRef(T defaultValue)
38+
{
39+
this.value = defaultValue;
40+
}
4441

45-
/// <summary>
46-
/// Allows an application to set a default scheduler for <see cref="IAsyncDatabaseConnection"/>
47-
/// instances created with <see cref="DatabaseConnection.AsAsyncDatabaseConnection(SQLiteDatabaseConnection)"/>.
48-
/// </summary>
49-
/// <remarks>This is a convenience feature that allows an application to set a global
50-
/// <see cref="IScheduler"/> instance, instead of supplying it with each call to
51-
/// <see cref="DatabaseConnection.AsAsyncDatabaseConnection(SQLiteDatabaseConnection)"/>.
52-
/// </remarks>
53-
/// <threadsafety static="false">This setter sets global state and should not be
54-
/// used after application initialization.</threadsafety>
55-
public static IScheduler DefaultScheduler
42+
public T Value
5643
{
57-
set
44+
get { return value; }
45+
46+
set
5847
{
59-
Contract.Requires(value != null);
60-
defaultScheduler = value;
48+
lock (gate)
49+
{
50+
this.value = value;
51+
}
6152
}
6253
}
63-
64-
internal static IAsyncDatabaseConnection AsAsyncDatabaseConnection(this SQLiteDatabaseConnection This, IScheduler scheduler, int interruptInstructionCount)
54+
55+
}
56+
57+
/// <summary>
58+
/// SQLiteDatabaseConnectionBuilder extension functions.
59+
/// </summary>
60+
public static class SQLiteDatabaseConnectionBuilderExtensions
61+
{
62+
/// <summary>
63+
/// Builds an IAsyncDatabaseConnection using the specified scheduler.
64+
/// </summary>
65+
/// <returns>An IAsyncDatabaseConnection using the specified scheduler.</returns>
66+
/// <param name="This">A SQLiteDatabaseConnectionBuilder instance.</param>
67+
/// <param name="scheduler">An RX scheduler</param>
68+
public static IAsyncDatabaseConnection BuildAsyncDatabaseConnection(
69+
this SQLiteDatabaseConnectionBuilder This,
70+
IScheduler scheduler)
6571
{
6672
Contract.Requires(This != null);
6773
Contract.Requires(scheduler != null);
6874

69-
IAsyncDatabaseConnection target;
70-
if (DatabaseConnectionExpando.Instance.GetOrAddValue(This, AsyncDatabaseConnectionKey, _ =>
71-
{
72-
// Store a WeakReference to the async connection so that we don't end up with a circular reference that prevents the
73-
// SQLiteDatabaseConnection from being freed.
74-
var asyncConnection = new AsyncDatabaseConnectionImpl(This, scheduler, interruptInstructionCount);
75-
return new WeakReference<IAsyncDatabaseConnection>(asyncConnection);
76-
}).TryGetTarget(out target))
77-
{
78-
return target;
79-
}
75+
var builder = This.Clone();
8076

81-
// This can't really happen.
82-
throw new InvalidOperationException();
83-
}
77+
var progressHandlerResult = new WriteLockedRef<bool>(false);
78+
builder.ProgressHandler = () => progressHandlerResult.Value;
79+
var db = This.Build();
8480

85-
/// <summary>
86-
/// Returns an <see cref="IAsyncDatabaseConnection"/> instance that delegates database requests
87-
/// to the provided <see cref="IDatabaseConnection"/>.
88-
/// </summary>
89-
/// <remarks>Note, once this method is called the provided <see cref="IDatabaseConnection"/>
90-
/// is owned by the returned <see cref="IAsyncDatabaseConnection"/>, and may no longer be
91-
/// safely used directly.</remarks>
92-
/// <param name="This">The database connection.</param>
93-
/// <param name="scheduler">A scheduler used to schedule asynchronous database use on.</param>
94-
/// <returns>An <see cref="IAsyncDatabaseConnection"/> instance.</returns>
95-
public static IAsyncDatabaseConnection AsAsyncDatabaseConnection(this SQLiteDatabaseConnection This, IScheduler scheduler) =>
96-
AsAsyncDatabaseConnection(This, scheduler, defaultInterruptInstructionCount);
81+
return new AsyncDatabaseConnectionImpl(db, scheduler, progressHandlerResult);
82+
}
9783

9884
/// <summary>
99-
/// Returns an <see cref="IAsyncDatabaseConnection"/> instance that delegates database requests
100-
/// to the provided <see cref="IDatabaseConnection"/>.
85+
/// Builds an IAsyncDatabaseConnection using the default TaskPool scheduler.
10186
/// </summary>
102-
/// <remarks>Note, once this method is called the provided <see cref="IDatabaseConnection"/>
103-
/// is owned by the returned <see cref="IAsyncDatabaseConnection"/>, and may no longer be
104-
/// safely used directly.</remarks>
105-
/// <param name="This">The database connection.</param>
106-
/// <returns>An <see cref="IAsyncDatabaseConnection"/> instance.</returns>
107-
public static IAsyncDatabaseConnection AsAsyncDatabaseConnection(this SQLiteDatabaseConnection This) =>
108-
AsAsyncDatabaseConnection(This, defaultScheduler);
87+
/// <returns>An IAsyncDatabaseConnection using the default TaskPool scheduler.</returns>
88+
/// <param name="This">A SQLiteDatabaseConnectionBuilder instance.</param>
89+
public static IAsyncDatabaseConnection BuildAsyncDatabaseConnection(this SQLiteDatabaseConnectionBuilder This) =>
90+
This.BuildAsyncDatabaseConnection(TaskPoolScheduler.Default);
10991
}
110-
92+
11193
/// <summary>
11294
/// Extensions methods for <see cref="IAsyncDatabaseConnection"/>.
11395
/// </summary>
@@ -491,19 +473,28 @@ internal sealed class AsyncDatabaseConnectionImpl : IAsyncDatabaseConnection
491473
{
492474
private readonly OperationsQueue queue = new OperationsQueue();
493475
private readonly IScheduler scheduler;
494-
private readonly int interruptInstructionCount;
495-
496476
private readonly SQLiteDatabaseConnection conn;
477+
private readonly WriteLockedRef<bool> progressHandlerResult;
497478

498479
private bool disposed = false;
499480

500-
internal AsyncDatabaseConnectionImpl(SQLiteDatabaseConnection conn, IScheduler scheduler, int interruptInstructionCount)
481+
internal AsyncDatabaseConnectionImpl(SQLiteDatabaseConnection conn, IScheduler scheduler, WriteLockedRef<bool> progressHandlerResult)
501482
{
502483
this.conn = conn;
503484
this.scheduler = scheduler;
504-
this.interruptInstructionCount = interruptInstructionCount;
485+
this.progressHandlerResult = progressHandlerResult;
486+
487+
this.Trace = Observable.FromEventPattern<DatabaseTraceEventArgs>(conn, "Trace").Select(e => e.EventArgs);
488+
this.Profile = Observable.FromEventPattern<DatabaseProfileEventArgs>(conn, "Profile").Select(e => e.EventArgs);
489+
this.Update = Observable.FromEventPattern<DatabaseUpdateEventArgs>(conn, "Update").Select(e => e.EventArgs);
505490
}
506491

492+
public IObservable<DatabaseTraceEventArgs> Trace { get; }
493+
494+
public IObservable<DatabaseProfileEventArgs> Profile { get; }
495+
496+
public IObservable<DatabaseUpdateEventArgs> Update { get; }
497+
507498
public async Task DisposeAsync()
508499
{
509500
if (disposed)
@@ -576,7 +567,9 @@ public IObservable<T> Use<T>(Func<IDatabaseConnection, CancellationToken, IEnume
576567

577568
return queue.EnqueueOperation(ct =>
578569
{
579-
this.conn.RegisterProgressHandler(interruptInstructionCount, () => ct.IsCancellationRequested);
570+
this.progressHandlerResult.Value = false;
571+
var ctSubscription = ct.Register(() => this.progressHandlerResult.Value = true);
572+
580573
try
581574
{
582575
ct.ThrowIfCancellationRequested();
@@ -597,7 +590,8 @@ public IObservable<T> Use<T>(Func<IDatabaseConnection, CancellationToken, IEnume
597590
}
598591
finally
599592
{
600-
this.conn.RemoveProgressHandler();
593+
ctSubscription.Dispose();
594+
this.progressHandlerResult.Value = false;
601595
}
602596
}, scheduler, cancellationToken);
603597
});

SQLitePCL.pretty.Async/Interfaces.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,27 @@ namespace SQLitePCL.pretty
2626
/// </summary>
2727
public interface IAsyncDatabaseConnection : IDisposable
2828
{
29+
/// <summary>
30+
/// A hot <see cref="IObservable&lt;DatabaseTraceEventArgs&gt;"/> of this connection's SQLite trace events.
31+
/// </summary>
32+
/// <seealso cref="SQLiteDatabaseConnection.Trace"/>
33+
/// <seealso href="https://sqlite.org/c3ref/profile.html"/>
34+
IObservable<DatabaseTraceEventArgs> Trace { get; }
35+
36+
/// <summary>
37+
/// A hot <see cref="IObservable&lt;DatabaseProfileEventArgs&gt;"/> of this connection's SQLite profile events.
38+
/// </summary>
39+
/// /// <seealso cref="SQLiteDatabaseConnection.Profile"/>
40+
/// <seealso href="https://sqlite.org/c3ref/profile.html"/>
41+
IObservable<DatabaseProfileEventArgs> Profile { get; }
42+
43+
/// <summary>
44+
/// A hot <see cref="IObservable&lt;DatabaseUpdateEventArgs&gt;"/> of this connection's SQLite update events.
45+
/// </summary>
46+
/// /// <seealso cref="SQLiteDatabaseConnection.Update"/>
47+
/// <seealso href="https://sqlite.org/c3ref/update_hook.html"/>
48+
IObservable<DatabaseUpdateEventArgs> Update { get; }
49+
2950
/// <summary>
3051
/// Shutdown the underlying operations queue used by the <see cref="IAsyncDatabaseConnection"/>
3152
/// and prevents the queuing of additional database access requests. Requests to access the database

SQLitePCL.pretty.tests/AsyncTests/AsyncBlobStreamTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public class AsyncBlobStreamTests
2828
[Fact]
2929
public async Task TestDispose()
3030
{
31-
using (var db = SQLite3.OpenInMemory().AsAsyncDatabaseConnection())
31+
using (var db = SQLiteDatabaseConnectionBuilder.InMemory().BuildAsyncDatabaseConnection())
3232
{
3333
await db.ExecuteAsync("CREATE TABLE foo (x blob);");
3434
await db.ExecuteAsync("INSERT INTO foo (x) VALUES(?);", "data");
@@ -53,7 +53,7 @@ await db.Query("SELECT rowid, x FROM foo")
5353
[Fact]
5454
public async Task TestRead()
5555
{
56-
using (var db = SQLite3.OpenInMemory().AsAsyncDatabaseConnection())
56+
using (var db = SQLiteDatabaseConnectionBuilder.InMemory().BuildAsyncDatabaseConnection())
5757
{
5858
byte[] bytes = new byte[1000];
5959
Random random = new Random();
@@ -89,7 +89,7 @@ public async Task TestRead()
8989
[Fact]
9090
public async Task TestWrite()
9191
{
92-
using (var db = SQLite3.OpenInMemory().AsAsyncDatabaseConnection())
92+
using (var db = SQLiteDatabaseConnectionBuilder.InMemory().BuildAsyncDatabaseConnection())
9393
{
9494
byte[] bytes = new byte[1000];
9595
Random random = new Random();
@@ -138,7 +138,7 @@ await db.Query("SELECT rowid, x FROM foo")
138138
[Fact]
139139
public async Task TestSeek()
140140
{
141-
using (var db = SQLite3.OpenInMemory().AsAsyncDatabaseConnection())
141+
using (var db = SQLiteDatabaseConnectionBuilder.InMemory().BuildAsyncDatabaseConnection())
142142
{
143143
await db.ExecuteAsync("CREATE TABLE foo (x blob);");
144144
await db.ExecuteAsync("INSERT INTO foo (x) VALUES(?);", "data");

SQLitePCL.pretty.tests/AsyncTests/AsyncDatabaseConnectionTests.cs

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,78 @@ namespace SQLitePCL.pretty.tests
2828
{
2929
public class AsyncDatabaseConnectionTests
3030
{
31+
[Fact]
32+
public async Task TestProfileEvent()
33+
{
34+
using (var db = SQLiteDatabaseConnectionBuilder.InMemory().BuildAsyncDatabaseConnection())
35+
{
36+
var statement = "CREATE TABLE foo (x int);";
37+
db.Profile.Subscribe(e =>
38+
{
39+
Assert.Equal(statement, e.Statement);
40+
Assert.True(TimeSpan.MinValue < e.ExecutionTime);
41+
});
42+
43+
await db.ExecuteAsync(statement);
44+
}
45+
}
46+
47+
[Fact]
48+
public async Task TestTraceEvent()
49+
{
50+
using (var db = SQLiteDatabaseConnectionBuilder.InMemory().BuildAsyncDatabaseConnection())
51+
{
52+
var statement = "CREATE TABLE foo (x int);";
53+
db.Trace.Subscribe(e =>
54+
{
55+
Assert.Equal(statement, e.Statement);
56+
});
57+
58+
await db.ExecuteAsync(statement);
59+
60+
statement = "INSERT INTO foo (x) VALUES (1);";
61+
await db.ExecuteAsync(statement);
62+
}
63+
}
64+
65+
[Fact]
66+
public async Task TestUpdateEvent()
67+
{
68+
using (var db = SQLiteDatabaseConnectionBuilder.InMemory().BuildAsyncDatabaseConnection())
69+
{
70+
var currentAction = ActionCode.CreateTable;
71+
var rowid = 1;
72+
73+
db.Update.Subscribe(e =>
74+
{
75+
Assert.Equal(currentAction, e.Action);
76+
Assert.Equal("main", e.Database);
77+
Assert.Equal("foo", e.Table);
78+
Assert.Equal(rowid, e.RowId);
79+
});
80+
81+
currentAction = ActionCode.CreateTable;
82+
rowid = 1;
83+
await db.ExecuteAsync("CREATE TABLE foo (x int);");
84+
85+
currentAction = ActionCode.Insert;
86+
rowid = 1;
87+
await db.ExecuteAsync("INSERT INTO foo (x) VALUES (1);");
88+
89+
currentAction = ActionCode.Insert;
90+
rowid = 2;
91+
await db.ExecuteAsync("INSERT INTO foo (x) VALUES (2);");
92+
93+
currentAction = ActionCode.DropTable;
94+
rowid = 2;
95+
await db.ExecuteAsync("DROP TABLE foo");
96+
}
97+
}
98+
3199
[Fact]
32100
public async Task TestUse()
33101
{
34-
using (var adb = SQLite3.OpenInMemory().AsAsyncDatabaseConnection())
102+
using (var adb = SQLiteDatabaseConnectionBuilder.InMemory().BuildAsyncDatabaseConnection())
35103
{
36104
await adb.Use(db => Enumerable.Range(0, 1000))
37105
.Scan(Tuple.Create(-1, -1), (x, y) => Tuple.Create(x.Item1 + 1, y))
@@ -63,7 +131,7 @@ await adb.Use(db => Enumerable.Range(0, 1000))
63131
[Fact]
64132
public async Task TestIDatabaseConnectionDispose()
65133
{
66-
using (var adb = SQLite3.OpenInMemory().AsAsyncDatabaseConnection())
134+
using (var adb = SQLiteDatabaseConnectionBuilder.InMemory().BuildAsyncDatabaseConnection())
67135
{
68136
await adb.ExecuteAsync("CREATE TABLE foo (x int);");
69137

@@ -124,7 +192,7 @@ await disposedObservable.Materialize()
124192
[Fact]
125193
public void TestUseCancelled()
126194
{
127-
using (var adb = SQLite3.OpenInMemory().AsAsyncDatabaseConnection())
195+
using (var adb = SQLiteDatabaseConnectionBuilder.InMemory().BuildAsyncDatabaseConnection())
128196
{
129197
var cts = new CancellationTokenSource();
130198
cts.Cancel();
@@ -138,7 +206,7 @@ public void TestUseCancelled()
138206
[Fact]
139207
public async Task TestPrepareAllAsync()
140208
{
141-
using (var adb = SQLite3.OpenInMemory().AsAsyncDatabaseConnection())
209+
using (var adb = SQLiteDatabaseConnectionBuilder.InMemory().BuildAsyncDatabaseConnection())
142210
{
143211
await adb.ExecuteAsync("CREATE TABLE foo (x int);");
144212
var stmts =
@@ -161,7 +229,7 @@ await adb.PrepareAllAsync(
161229
[Fact]
162230
public async Task TestQuery()
163231
{
164-
using (var adb = SQLite3.OpenInMemory().AsAsyncDatabaseConnection())
232+
using (var adb = SQLiteDatabaseConnectionBuilder.InMemory().BuildAsyncDatabaseConnection())
165233
{
166234
var _0 = "hello";
167235
var _1 = 1;
@@ -181,7 +249,10 @@ await adb.Query("Select ?, ?, ?", _0, _1, _2)
181249
[Fact]
182250
public void TestStatementCancellation()
183251
{
184-
using (var adb = SQLite3.OpenInMemory().AsAsyncDatabaseConnection(TaskPoolScheduler.Default, 1))
252+
var builder = SQLiteDatabaseConnectionBuilder.InMemory();
253+
builder.ProgressHandlerInterval = 1;
254+
255+
using (var adb = builder.BuildAsyncDatabaseConnection(TaskPoolScheduler.Default))
185256
{
186257
var cts = new CancellationTokenSource();
187258
Assert.ThrowsAsync<TaskCanceledException>(async () =>

0 commit comments

Comments
 (0)