Skip to content

Commit

Permalink
Add support for remote database connection (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
tvandinther authored Jan 27, 2024
1 parent 27eb76a commit 4b15af3
Show file tree
Hide file tree
Showing 22 changed files with 359 additions and 83 deletions.
9 changes: 8 additions & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,12 @@ jobs:
- name: Build
run: dotnet build --configuration Release --no-restore

- name: Setup Docker on macOS
uses: douglascamata/setup-docker-macos-action@v1-alpha
if: matrix.os == 'macos-latest'

- name: Test
run: dotnet test --no-restore --verbosity normal
run: docker compose up -d && dotnet test --no-restore --verbosity normal
env:
LIBSQL_TEST_URL: http://localhost:8080
LIBSQL_TEST_AUTH_TOKEN: ""
2 changes: 1 addition & 1 deletion Demo/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Libsql.Client;

var dbClient = DatabaseClient.Create(opts => {
var dbClient = await DatabaseClient.Create(opts => {
opts.Url = ":memory:";
});

Expand Down
34 changes: 34 additions & 0 deletions Libsql.Client.Tests/EmbeddedReplicaTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace Libsql.Client.Tests;

public class EmbeddedReplicaTests
{
[Fact(Skip = "Not implemented")]
public async Task CanConnectAndQueryReplicaDatabase()
{
var db = await DatabaseClient.Create(opts => {
opts.Url = Environment.GetEnvironmentVariable("LIBSQL_TEST_URL") ?? throw new InvalidOperationException("LIBSQL_TEST_URL is not set");
opts.AuthToken = Environment.GetEnvironmentVariable("LIBSQL_TEST_AUTH_TOKEN");
opts.ReplicaPath = "/home/tvandinther/code/libsql-client-dotnet/replica.db";
});

await db.Sync();

var rs = await db.Execute("SELECT COUNT(*) FROM albums");

var count = rs.Rows.First().First();
var value = Assert.IsType<Integer>(count);
Assert.Equal(347, value.Value);
}

[Fact(Skip = "Not implemented")]
public async Task CanCallSync()
{
var db = await DatabaseClient.Create(opts => {
opts.Url = Environment.GetEnvironmentVariable("LIBSQL_TEST_URL") ?? throw new InvalidOperationException("LIBSQL_TEST_URL is not set");
opts.AuthToken = Environment.GetEnvironmentVariable("LIBSQL_TEST_AUTH_TOKEN");
opts.ReplicaPath = "/home/tvandinther/code/libsql-client-dotnet/replica.db";
});

await db.Sync();
}
}
20 changes: 20 additions & 0 deletions Libsql.Client.Tests/RemoteTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Libsql.Client.Tests;

public class RemoteTests
{
[Fact]
public async Task CanConnectAndQueryRemoteDatabase()
{
var db = await DatabaseClient.Create(opts => {
opts.Url = Environment.GetEnvironmentVariable("LIBSQL_TEST_URL") ?? throw new InvalidOperationException("LIBSQL_TEST_URL is not set");
opts.AuthToken = Environment.GetEnvironmentVariable("LIBSQL_TEST_AUTH_TOKEN");
});

var rs = await db.Execute("SELECT COUNT(*) FROM tracks");

var count = rs.Rows.First().First();
var value = Assert.IsType<Integer>(count);
Console.WriteLine(value.Value);
Assert.Equal(3503, value.Value);
}
}
2 changes: 1 addition & 1 deletion Libsql.Client.Tests/ResultSetTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ namespace Libsql.Client.Tests;

public class ResultSetTests
{
private readonly IDatabaseClient _db = DatabaseClient.Create();
private readonly IDatabaseClient _db = DatabaseClient.Create().Result;

[Fact]
public async Task Columns_EmptyEnumerable_WhenNonQuery()
Expand Down
2 changes: 1 addition & 1 deletion Libsql.Client.Tests/RowsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

public class RowsTests
{
private readonly IDatabaseClient _db = DatabaseClient.Create();
private readonly IDatabaseClient _db = DatabaseClient.Create().Result;

[Fact]
public async Task Rows_WhenEmpty()
Expand Down
2 changes: 1 addition & 1 deletion Libsql.Client.Tests/SelectTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ namespace Libsql.Client.Tests;

public class SelectTests
{
private readonly IDatabaseClient _db = DatabaseClient.Create();
private readonly IDatabaseClient _db = DatabaseClient.Create().Result;

[Fact]
public async Task SelectIntType()
Expand Down
20 changes: 18 additions & 2 deletions Libsql.Client/DatabaseClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;

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

return new DatabaseWrapper(options);
var DatabaseWrapper = new DatabaseWrapper(options);
await DatabaseWrapper.Open();
DatabaseWrapper.Connect();

return DatabaseWrapper;
}

/// <summary>
/// Creates a new instance of the <see cref="IDatabaseClient"/> interface.
/// </summary>
/// <param name="path">The path to the database file.</param>
/// <returns>A new instance of the <see cref="IDatabaseClient"/> interface.</returns>
/// <remarks>An overload for opening a local database file. Equivalent to setting only the Url property of the options.</remarks>
public static async Task<IDatabaseClient> Create(string path)
{
return await Create(opts => opts.Url = path);
}
}
}
10 changes: 9 additions & 1 deletion Libsql.Client/DatabaseClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
/// </summary>
public class DatabaseClientOptions
{
private DatabaseClientOptions(string url, string authToken = null, bool useHttps = false)
private DatabaseClientOptions(string url, string authToken = null, string replicaPath = null, bool useHttps = true)
{
Url = url;
AuthToken = authToken;
ReplicaPath = replicaPath;
UseHttps = useHttps;
}

Expand All @@ -24,10 +25,17 @@ private DatabaseClientOptions(string url, string authToken = null, bool useHttps
/// Gets or sets the authentication token used to connect to the database.
/// </summary>
public string AuthToken { get; set; }

/// <summary>
/// Gets or sets the path to the replica database file.
/// </summary>
/// <remarks>Default: <c>null</c>. If set, the database will be replicated to the specified file.</remarks>
public string ReplicaPath { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to use HTTPS protocol for database connections.
/// </summary>
/// <remarks>Default: <c>true</c>.</remarks>
public bool UseHttps { get; set; }
}
}
9 changes: 9 additions & 0 deletions Libsql.Client/DatabaseType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Libsql.Client{
internal enum DatabaseType
{
Memory,
File,
Remote,
EmbeddedReplica
}
}
114 changes: 102 additions & 12 deletions Libsql.Client/DatabaseWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,48 +10,117 @@ internal class DatabaseWrapper : IDatabaseClient, IDisposable
{
private libsql_database_t _db;
private libsql_connection_t _connection;
private readonly DatabaseClientOptions _options;
private readonly DatabaseType _type;

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

if (!(options.Url == "" || options.Url == ":memory:"))
_options = options;

if (options.Url == "" || options.Url == ":memory:")
{
_type = DatabaseType.Memory;
}
else
{
try
{
var uri = new Uri(options.Url);

switch (uri.Scheme)
{
case "http":
case "https":
case "ws":
case "wss":
throw new LibsqlException($"{uri.Scheme}:// is not yet supported");
_type = _options.ReplicaPath != null ? DatabaseType.EmbeddedReplica : DatabaseType.Remote;
break;
default:
throw new InvalidOperationException($"Unsupported scheme: {uri.Scheme}");
}
}
catch (UriFormatException) { }
catch (UriFormatException)
{
_type = DatabaseType.File;
}
}
}

// C# empty strings have null pointers, so we need to give the url some meat.
var url = options.Url is "" ? "\0" : options.Url;

internal async Task Open()
{
var error = new Error();
int exitCode;
switch (_type)
{
case DatabaseType.Memory:
case DatabaseType.File:
exitCode = OpenMemoryOrFileDatabase(_options, error);
break;
case DatabaseType.Remote:
case DatabaseType.EmbeddedReplica:
exitCode = await Task.Run(() => OpenRemoteDatabase(_options, ref error));
break;
default:
throw new InvalidOperationException($"Unsupported database type: {_type}");
}

error.ThrowIfNonZero(exitCode, "Failed to open database");
}

private unsafe int OpenMemoryOrFileDatabase(DatabaseClientOptions options, Error error)
{
// C# empty strings have null pointers, so we need to give the url some meat.
var url = options.Url is "" ? "\0" : options.Url;

fixed (libsql_database_t* dbPtr = &_db)
{
fixed (byte* urlPtr = Encoding.UTF8.GetBytes(url))
{
exitCode = Bindings.libsql_open_ext(urlPtr, dbPtr, &error.Ptr);
return Bindings.libsql_open_ext(urlPtr, dbPtr, &error.Ptr);
}
}
}

private unsafe int OpenRemoteDatabase(DatabaseClientOptions options, ref Error error)
{
var url = options.Url;
var authToken = options.AuthToken;
var replicaPath = options.ReplicaPath;
var useHttps = options.UseHttps;

fixed (libsql_database_t* dbPtr = &_db)
{
fixed (byte* urlPtr = Encoding.UTF8.GetBytes(url))
{
if (string.IsNullOrEmpty(authToken)) authToken = "\0";
fixed (byte* authTokenPtr = Encoding.UTF8.GetBytes(authToken))
{
fixed (byte** errorCodePtr = &error.Ptr) {
if (replicaPath is null)
{
var exitCode = Bindings.libsql_open_remote(urlPtr, authTokenPtr, dbPtr, errorCodePtr);
return exitCode;
}

fixed (byte* replicaPathPtr = Encoding.UTF8.GetBytes(replicaPath))
{
return Bindings.libsql_open_sync(
replicaPathPtr,
urlPtr,
authTokenPtr,
dbPtr,
errorCodePtr
);
}
}
}
}
}

error.ThrowIfNonZero(exitCode, "Failed to open database");

Connect();
}

private unsafe void Connect()
internal unsafe void Connect()
{
var error = new Error();
int exitCode;
Expand Down Expand Up @@ -96,6 +165,27 @@ public Task<IResultSet> Execute(string sql, params object[] args)
throw new NotImplementedException();
}

public async Task Sync()
{
if (_type != DatabaseType.EmbeddedReplica)
{
throw new InvalidOperationException("Cannot sync a non-replica database");
}

await Task.Run(() =>
{
unsafe
{
var error = new Error();
int exitCode;
exitCode = Bindings.libsql_sync(_db, &error.Ptr);
error.ThrowIfNonZero(exitCode, "Failed to sync database");
}
});
}

private void ReleaseUnmanagedResources()
{
Bindings.libsql_disconnect(_connection);
Expand Down
1 change: 0 additions & 1 deletion Libsql.Client/Error.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Runtime.InteropServices;
using Libsql.Client.Extensions;

namespace Libsql.Client
Expand Down
6 changes: 6 additions & 0 deletions Libsql.Client/IDatabaseClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,11 @@ public interface IDatabaseClient
/// <returns>The result set returned by the query.</returns>
/// <exception cref="LibsqlException">Thrown when the query fails to execute.</exception>
Task<IResultSet> Execute(string sql, params object[] args);

/// <summary>
/// Synchronises the embedded replica database with the remote database.
/// </summary>
/// <exception cref="LibsqlException">Thrown when the synchronisation fails.</exception>
Task Sync();
}
}
2 changes: 1 addition & 1 deletion Libsql.Client/Libsql.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<Title>Libsql.Client</Title>
<Authors>Tom van Dinther</Authors>
<Description>A client library for Libsql.</Description>
<PackageVersion>0.3.0</PackageVersion>
<PackageVersion>0.4.0</PackageVersion>
<Copyright>Copyright (c) Tom van Dinther 2023</Copyright>
<PackageProjectUrl>https://github.com/tvandinther/libsql-client-dotnet</PackageProjectUrl>
<PackageLicense>https://raw.githubusercontent.com/tvandinther/libsql-client-dotnet/master/LICENSE</PackageLicense>
Expand Down
Loading

0 comments on commit 4b15af3

Please sign in to comment.