Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for remote database connection #13

Merged
merged 13 commits into from
Jan 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading