Skip to content

Commit 4b15af3

Browse files
authored
Add support for remote database connection (#13)
1 parent 27eb76a commit 4b15af3

22 files changed

+359
-83
lines changed

.github/workflows/test.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,12 @@ jobs:
4040
- name: Build
4141
run: dotnet build --configuration Release --no-restore
4242

43+
- name: Setup Docker on macOS
44+
uses: douglascamata/setup-docker-macos-action@v1-alpha
45+
if: matrix.os == 'macos-latest'
46+
4347
- name: Test
44-
run: dotnet test --no-restore --verbosity normal
48+
run: docker compose up -d && dotnet test --no-restore --verbosity normal
49+
env:
50+
LIBSQL_TEST_URL: http://localhost:8080
51+
LIBSQL_TEST_AUTH_TOKEN: ""

Demo/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using Libsql.Client;
22

3-
var dbClient = DatabaseClient.Create(opts => {
3+
var dbClient = await DatabaseClient.Create(opts => {
44
opts.Url = ":memory:";
55
});
66

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace Libsql.Client.Tests;
2+
3+
public class EmbeddedReplicaTests
4+
{
5+
[Fact(Skip = "Not implemented")]
6+
public async Task CanConnectAndQueryReplicaDatabase()
7+
{
8+
var db = await DatabaseClient.Create(opts => {
9+
opts.Url = Environment.GetEnvironmentVariable("LIBSQL_TEST_URL") ?? throw new InvalidOperationException("LIBSQL_TEST_URL is not set");
10+
opts.AuthToken = Environment.GetEnvironmentVariable("LIBSQL_TEST_AUTH_TOKEN");
11+
opts.ReplicaPath = "/home/tvandinther/code/libsql-client-dotnet/replica.db";
12+
});
13+
14+
await db.Sync();
15+
16+
var rs = await db.Execute("SELECT COUNT(*) FROM albums");
17+
18+
var count = rs.Rows.First().First();
19+
var value = Assert.IsType<Integer>(count);
20+
Assert.Equal(347, value.Value);
21+
}
22+
23+
[Fact(Skip = "Not implemented")]
24+
public async Task CanCallSync()
25+
{
26+
var db = await DatabaseClient.Create(opts => {
27+
opts.Url = Environment.GetEnvironmentVariable("LIBSQL_TEST_URL") ?? throw new InvalidOperationException("LIBSQL_TEST_URL is not set");
28+
opts.AuthToken = Environment.GetEnvironmentVariable("LIBSQL_TEST_AUTH_TOKEN");
29+
opts.ReplicaPath = "/home/tvandinther/code/libsql-client-dotnet/replica.db";
30+
});
31+
32+
await db.Sync();
33+
}
34+
}

Libsql.Client.Tests/RemoteTests.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace Libsql.Client.Tests;
2+
3+
public class RemoteTests
4+
{
5+
[Fact]
6+
public async Task CanConnectAndQueryRemoteDatabase()
7+
{
8+
var db = await DatabaseClient.Create(opts => {
9+
opts.Url = Environment.GetEnvironmentVariable("LIBSQL_TEST_URL") ?? throw new InvalidOperationException("LIBSQL_TEST_URL is not set");
10+
opts.AuthToken = Environment.GetEnvironmentVariable("LIBSQL_TEST_AUTH_TOKEN");
11+
});
12+
13+
var rs = await db.Execute("SELECT COUNT(*) FROM tracks");
14+
15+
var count = rs.Rows.First().First();
16+
var value = Assert.IsType<Integer>(count);
17+
Console.WriteLine(value.Value);
18+
Assert.Equal(3503, value.Value);
19+
}
20+
}

Libsql.Client.Tests/ResultSetTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ namespace Libsql.Client.Tests;
22

33
public class ResultSetTests
44
{
5-
private readonly IDatabaseClient _db = DatabaseClient.Create();
5+
private readonly IDatabaseClient _db = DatabaseClient.Create().Result;
66

77
[Fact]
88
public async Task Columns_EmptyEnumerable_WhenNonQuery()

Libsql.Client.Tests/RowsTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
public class RowsTests
44
{
5-
private readonly IDatabaseClient _db = DatabaseClient.Create();
5+
private readonly IDatabaseClient _db = DatabaseClient.Create().Result;
66

77
[Fact]
88
public async Task Rows_WhenEmpty()

Libsql.Client.Tests/SelectTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ namespace Libsql.Client.Tests;
22

33
public class SelectTests
44
{
5-
private readonly IDatabaseClient _db = DatabaseClient.Create();
5+
private readonly IDatabaseClient _db = DatabaseClient.Create().Result;
66

77
[Fact]
88
public async Task SelectIntType()

Libsql.Client/DatabaseClient.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Threading.Tasks;
23

34
namespace Libsql.Client
45
{
@@ -15,13 +16,28 @@ public static class DatabaseClient
1516
/// <remarks>A client constitutes a connection to the database.</remarks>
1617
/// <exception cref="ArgumentNullException">Thrown when <paramref name="configure"/> is null.</exception>
1718
/// <exception cref="LibsqlException">Thrown when the database fails to open and/or connect.</exception>
18-
public static IDatabaseClient Create(Action<DatabaseClientOptions> configure = default)
19+
public static async Task<IDatabaseClient> Create(Action<DatabaseClientOptions> configure = default)
1920
{
2021
var options = DatabaseClientOptions.Default;
2122
configure?.Invoke(options);
2223
if (options.Url is null) throw new ArgumentNullException(nameof(options.Url));
2324

24-
return new DatabaseWrapper(options);
25+
var DatabaseWrapper = new DatabaseWrapper(options);
26+
await DatabaseWrapper.Open();
27+
DatabaseWrapper.Connect();
28+
29+
return DatabaseWrapper;
30+
}
31+
32+
/// <summary>
33+
/// Creates a new instance of the <see cref="IDatabaseClient"/> interface.
34+
/// </summary>
35+
/// <param name="path">The path to the database file.</param>
36+
/// <returns>A new instance of the <see cref="IDatabaseClient"/> interface.</returns>
37+
/// <remarks>An overload for opening a local database file. Equivalent to setting only the Url property of the options.</remarks>
38+
public static async Task<IDatabaseClient> Create(string path)
39+
{
40+
return await Create(opts => opts.Url = path);
2541
}
2642
}
2743
}

Libsql.Client/DatabaseClientOptions.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
/// </summary>
66
public class DatabaseClientOptions
77
{
8-
private DatabaseClientOptions(string url, string authToken = null, bool useHttps = false)
8+
private DatabaseClientOptions(string url, string authToken = null, string replicaPath = null, bool useHttps = true)
99
{
1010
Url = url;
1111
AuthToken = authToken;
12+
ReplicaPath = replicaPath;
1213
UseHttps = useHttps;
1314
}
1415

@@ -24,10 +25,17 @@ private DatabaseClientOptions(string url, string authToken = null, bool useHttps
2425
/// Gets or sets the authentication token used to connect to the database.
2526
/// </summary>
2627
public string AuthToken { get; set; }
28+
29+
/// <summary>
30+
/// Gets or sets the path to the replica database file.
31+
/// </summary>
32+
/// <remarks>Default: <c>null</c>. If set, the database will be replicated to the specified file.</remarks>
33+
public string ReplicaPath { get; set; }
2734

2835
/// <summary>
2936
/// Gets or sets a value indicating whether to use HTTPS protocol for database connections.
3037
/// </summary>
38+
/// <remarks>Default: <c>true</c>.</remarks>
3139
public bool UseHttps { get; set; }
3240
}
3341
}

Libsql.Client/DatabaseType.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Libsql.Client{
2+
internal enum DatabaseType
3+
{
4+
Memory,
5+
File,
6+
Remote,
7+
EmbeddedReplica
8+
}
9+
}

Libsql.Client/DatabaseWrapper.cs

Lines changed: 102 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,48 +10,117 @@ internal class DatabaseWrapper : IDatabaseClient, IDisposable
1010
{
1111
private libsql_database_t _db;
1212
private libsql_connection_t _connection;
13+
private readonly DatabaseClientOptions _options;
14+
private readonly DatabaseType _type;
1315

1416
public unsafe DatabaseWrapper(DatabaseClientOptions options)
1517
{
1618
Debug.Assert(options.Url != null, "url is null");
1719

18-
if (!(options.Url == "" || options.Url == ":memory:"))
20+
_options = options;
21+
22+
if (options.Url == "" || options.Url == ":memory:")
23+
{
24+
_type = DatabaseType.Memory;
25+
}
26+
else
1927
{
2028
try
2129
{
2230
var uri = new Uri(options.Url);
31+
2332
switch (uri.Scheme)
2433
{
2534
case "http":
2635
case "https":
2736
case "ws":
2837
case "wss":
29-
throw new LibsqlException($"{uri.Scheme}:// is not yet supported");
38+
_type = _options.ReplicaPath != null ? DatabaseType.EmbeddedReplica : DatabaseType.Remote;
39+
break;
40+
default:
41+
throw new InvalidOperationException($"Unsupported scheme: {uri.Scheme}");
3042
}
3143
}
32-
catch (UriFormatException) { }
44+
catch (UriFormatException)
45+
{
46+
_type = DatabaseType.File;
47+
}
3348
}
49+
}
3450

35-
// C# empty strings have null pointers, so we need to give the url some meat.
36-
var url = options.Url is "" ? "\0" : options.Url;
37-
51+
internal async Task Open()
52+
{
3853
var error = new Error();
3954
int exitCode;
55+
switch (_type)
56+
{
57+
case DatabaseType.Memory:
58+
case DatabaseType.File:
59+
exitCode = OpenMemoryOrFileDatabase(_options, error);
60+
break;
61+
case DatabaseType.Remote:
62+
case DatabaseType.EmbeddedReplica:
63+
exitCode = await Task.Run(() => OpenRemoteDatabase(_options, ref error));
64+
break;
65+
default:
66+
throw new InvalidOperationException($"Unsupported database type: {_type}");
67+
}
4068

69+
error.ThrowIfNonZero(exitCode, "Failed to open database");
70+
}
71+
72+
private unsafe int OpenMemoryOrFileDatabase(DatabaseClientOptions options, Error error)
73+
{
74+
// C# empty strings have null pointers, so we need to give the url some meat.
75+
var url = options.Url is "" ? "\0" : options.Url;
76+
4177
fixed (libsql_database_t* dbPtr = &_db)
4278
{
4379
fixed (byte* urlPtr = Encoding.UTF8.GetBytes(url))
4480
{
45-
exitCode = Bindings.libsql_open_ext(urlPtr, dbPtr, &error.Ptr);
81+
return Bindings.libsql_open_ext(urlPtr, dbPtr, &error.Ptr);
82+
}
83+
}
84+
}
85+
86+
private unsafe int OpenRemoteDatabase(DatabaseClientOptions options, ref Error error)
87+
{
88+
var url = options.Url;
89+
var authToken = options.AuthToken;
90+
var replicaPath = options.ReplicaPath;
91+
var useHttps = options.UseHttps;
92+
93+
fixed (libsql_database_t* dbPtr = &_db)
94+
{
95+
fixed (byte* urlPtr = Encoding.UTF8.GetBytes(url))
96+
{
97+
if (string.IsNullOrEmpty(authToken)) authToken = "\0";
98+
fixed (byte* authTokenPtr = Encoding.UTF8.GetBytes(authToken))
99+
{
100+
fixed (byte** errorCodePtr = &error.Ptr) {
101+
if (replicaPath is null)
102+
{
103+
var exitCode = Bindings.libsql_open_remote(urlPtr, authTokenPtr, dbPtr, errorCodePtr);
104+
return exitCode;
105+
}
106+
107+
fixed (byte* replicaPathPtr = Encoding.UTF8.GetBytes(replicaPath))
108+
{
109+
return Bindings.libsql_open_sync(
110+
replicaPathPtr,
111+
urlPtr,
112+
authTokenPtr,
113+
dbPtr,
114+
errorCodePtr
115+
);
116+
}
117+
}
118+
}
46119
}
47120
}
48-
49-
error.ThrowIfNonZero(exitCode, "Failed to open database");
50-
51-
Connect();
52121
}
53122

54-
private unsafe void Connect()
123+
internal unsafe void Connect()
55124
{
56125
var error = new Error();
57126
int exitCode;
@@ -96,6 +165,27 @@ public Task<IResultSet> Execute(string sql, params object[] args)
96165
throw new NotImplementedException();
97166
}
98167

168+
public async Task Sync()
169+
{
170+
if (_type != DatabaseType.EmbeddedReplica)
171+
{
172+
throw new InvalidOperationException("Cannot sync a non-replica database");
173+
}
174+
175+
await Task.Run(() =>
176+
{
177+
unsafe
178+
{
179+
var error = new Error();
180+
int exitCode;
181+
182+
exitCode = Bindings.libsql_sync(_db, &error.Ptr);
183+
184+
error.ThrowIfNonZero(exitCode, "Failed to sync database");
185+
}
186+
});
187+
}
188+
99189
private void ReleaseUnmanagedResources()
100190
{
101191
Bindings.libsql_disconnect(_connection);

Libsql.Client/Error.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System;
2-
using System.Runtime.InteropServices;
32
using Libsql.Client.Extensions;
43

54
namespace Libsql.Client

Libsql.Client/IDatabaseClient.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,11 @@ public interface IDatabaseClient
2323
/// <returns>The result set returned by the query.</returns>
2424
/// <exception cref="LibsqlException">Thrown when the query fails to execute.</exception>
2525
Task<IResultSet> Execute(string sql, params object[] args);
26+
27+
/// <summary>
28+
/// Synchronises the embedded replica database with the remote database.
29+
/// </summary>
30+
/// <exception cref="LibsqlException">Thrown when the synchronisation fails.</exception>
31+
Task Sync();
2632
}
2733
}

Libsql.Client/Libsql.Client.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<Title>Libsql.Client</Title>
55
<Authors>Tom van Dinther</Authors>
66
<Description>A client library for Libsql.</Description>
7-
<PackageVersion>0.3.0</PackageVersion>
7+
<PackageVersion>0.4.0</PackageVersion>
88
<Copyright>Copyright (c) Tom van Dinther 2023</Copyright>
99
<PackageProjectUrl>https://github.com/tvandinther/libsql-client-dotnet</PackageProjectUrl>
1010
<PackageLicense>https://raw.githubusercontent.com/tvandinther/libsql-client-dotnet/master/LICENSE</PackageLicense>

0 commit comments

Comments
 (0)