Skip to content

Commit

Permalink
Feat/live query (surrealdb#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
Odonno authored Nov 10, 2023
1 parent ea27e3a commit 74d69b9
Show file tree
Hide file tree
Showing 47 changed files with 2,992 additions and 54 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,10 @@ jobs:
- name: Build
run: dotnet build --no-restore --warnaserror

- name: Test
- name: Test library
working-directory: ./SurrealDb.Net.Tests
run: dotnet test --no-build

- name: Test Live Query feature
working-directory: ./SurrealDb.Net.LiveQuery.Tests
run: dotnet test --no-build
12 changes: 6 additions & 6 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<ItemGroup>
<PackageReference Include="CSharpier.MsBuild" Version="0.25.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="CSharpier.MsBuild" Version="0.25.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
60 changes: 35 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ dotnet add package SurrealDb.Net

Supported protocols:

- ✅ HTTP(S)
- ✅ WS(S)
- 🚧 and more to come...
- ✅ HTTP(S)
- ✅ WS(S)
- 🚧 and more to come...

#### Construct a new SurrealDB client

Expand Down Expand Up @@ -100,16 +100,16 @@ Consider the following `appsettings.json` file:

```json
{
"AllowedHosts": "*",
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"SurrealDB": "Server=http://localhost:8000;Namespace=test;Database=test;Username=root;Password=root"
}
"AllowedHosts": "*",
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"SurrealDB": "Server=http://localhost:8000;Namespace=test;Database=test;Username=root;Password=root"
}
}
```

Expand All @@ -136,9 +136,9 @@ services.AddSurreal<IMonitoringSurrealDbClient>(configuration.GetConnectionStrin

Here you will have 3 instances:

- the default one, you can keep using `ISurrealDbClient` interface or `SurrealDbClient` class anywhere
- a client for backup purpose, using the `IBackupSurrealDbClient` interface
- a client for monitoring purpose, using the `IMonitoringSurrealDbClient` interface
- the default one, you can keep using `ISurrealDbClient` interface or `SurrealDbClient` class anywhere
- a client for backup purpose, using the `IBackupSurrealDbClient` interface
- a client for monitoring purpose, using the `IMonitoringSurrealDbClient` interface

#### Use the client

Expand Down Expand Up @@ -231,20 +231,20 @@ public class WeatherForecastController : ControllerBase

This project was written following testing best practices:

- TDD, leveraging:
- clean code/architecture
- regression testing
- adding new features and tests easily
- xUnit and FluentAssertions libraries
- a vast majority of tests are integration tests, ensuring compatibility with a concrete SurrealDB version
- each integration test is using a separate SurrealDB instance
- TDD, leveraging:
- clean code/architecture
- regression testing
- adding new features and tests easily
- xUnit and FluentAssertions libraries
- a vast majority of tests are integration tests, ensuring compatibility with a concrete SurrealDB version
- each integration test is using a separate SurrealDB instance

### .NET release versions

The .NET release versions must follow these rules:

- Should target at least the latest LTS (Long-Term Support) version
- Should target at least the latest STS (Standard-Term Support) version
- Should target at least the latest LTS (Long-Term Support) version
- Should target at least the latest STS (Standard-Term Support) version

SurrealDb.Net targets .NET versions following the [.NET Support Policy by Microsoft](https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core). Additionally, SurrealDb.Net targets .NET Standard 2.1 explicitly to continue support of the Mono runtime (Unity, Xamarin, etc...).

Expand Down Expand Up @@ -272,6 +272,16 @@ Once ready, go to the root directory of the project and run the following comman
dotnet watch test --project SurrealDb.Net.Tests
```

Due to the asynchronous nature of Live Queries, they are tested against a separate project named `SurrealDb.Net.LiveQuery.Tests`. Where the default test project allow full parallelization, this project completely disable test parallelization. To execute tests on Live Queries, run the following command:

```
dotnet watch test --project SurrealDb.Net.LiveQuery.Tests
```

Note 1: Because Live Query tests are not run in parallel, it can take quite some time to run all tests.

Note 2: You can run the two test projects in parallel.

### Benchmarking

This project also contains [benchmarks](https://benchmarkdotnet.org/) in order to detect possible performance regressions.
Expand Down
16 changes: 16 additions & 0 deletions SurrealDb.Net.LiveQuery.Tests/Abstract/BaseLiveQueryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace SurrealDb.Net.LiveQuery.Tests.Abstract;

public abstract class BaseLiveQueryTests
{
protected static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2);

protected Task WaitLiveQueryCreationAsync(int timeMultiplier = 1)
{
return Task.Delay(100 * timeMultiplier);
}

protected Task WaitLiveQueryNotificationAsync()
{
return Task.Delay(100);
}
}
82 changes: 82 additions & 0 deletions SurrealDb.Net.LiveQuery.Tests/KillTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using SurrealDb.Net.Exceptions;
using SurrealDb.Net.Models.Response;

namespace SurrealDb.Net.LiveQuery.Tests;

public class KillTests
{
[Fact]
public async Task ShouldNotBeSupportedOnHttpProtocol()
{
const string url = "http://localhost:8000";

Func<Task> func = async () =>
{
await using var surrealDbClientGenerator = new SurrealDbClientGenerator();
var dbInfo = surrealDbClientGenerator.GenerateDatabaseInfo();

using var client = surrealDbClientGenerator.Create(url);
await client.SignIn(new RootAuth { Username = "root", Password = "root" });
await client.Use(dbInfo.Namespace, dbInfo.Database);

var liveQueryUuid = Guid.NewGuid();

await client.Kill(liveQueryUuid);
};

await func.Should().ThrowAsync<NotSupportedException>();
}

[Fact]
public async Task ShouldKillActiveLiveQueryOnWsProtocol()
{
const string url = "ws://localhost:8000/rpc";

Func<Task> func = async () =>
{
await using var surrealDbClientGenerator = new SurrealDbClientGenerator();
var dbInfo = surrealDbClientGenerator.GenerateDatabaseInfo();

using var client = surrealDbClientGenerator.Create(url);
await client.SignIn(new RootAuth { Username = "root", Password = "root" });
await client.Use(dbInfo.Namespace, dbInfo.Database);

var response = await client.Query("LIVE SELECT * FROM test;");

if (response.FirstResult is not SurrealDbOkResult okResult)
throw new Exception("Expected a SurrealDbOkResult");

var liveQueryUuid = okResult.GetValue<Guid>();

await client.Kill(liveQueryUuid);
};

await func.Should().NotThrowAsync();
}

[Fact]
public async Task ShouldFailToKillInexistantLiveQueryOnWsProtocol()
{
const string url = "ws://localhost:8000/rpc";

Func<Task> func = async () =>
{
await using var surrealDbClientGenerator = new SurrealDbClientGenerator();
var dbInfo = surrealDbClientGenerator.GenerateDatabaseInfo();

using var client = surrealDbClientGenerator.Create(url);
await client.SignIn(new RootAuth { Username = "root", Password = "root" });
await client.Use(dbInfo.Namespace, dbInfo.Database);

var liveQueryUuid = Guid.NewGuid();

await client.Kill(liveQueryUuid);
};

await func.Should()
.ThrowAsync<SurrealDbException>()
.WithMessage(
"There was a problem with the database: Can not execute KILL statement using id '$id'"
);
}
}
111 changes: 111 additions & 0 deletions SurrealDb.Net.LiveQuery.Tests/ListenLiveTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using SurrealDb.Net.LiveQuery.Tests.Abstract;
using SurrealDb.Net.LiveQuery.Tests.Models;
using SurrealDb.Net.Models.LiveQuery;
using SurrealDb.Net.Models.Response;

namespace SurrealDb.Net.LiveQuery.Tests;

public class ListenLiveTests : BaseLiveQueryTests
{
[Fact]
public async Task ShouldNotBeSupportedOnHttpProtocol()
{
const string url = "http://localhost:8000";

Func<Task> func = async () =>
{
await using var surrealDbClientGenerator = new SurrealDbClientGenerator();
var dbInfo = surrealDbClientGenerator.GenerateDatabaseInfo();

using var client = surrealDbClientGenerator.Create(url);
await client.SignIn(new RootAuth { Username = "root", Password = "root" });
await client.Use(dbInfo.Namespace, dbInfo.Database);

var liveQueryUuid = Guid.NewGuid();

await using var liveQuery = client.ListenLive<int>(liveQueryUuid);
};

await func.Should().ThrowAsync<NotSupportedException>();
}

[Fact]
public async Task ShouldReceiveData()
{
const string url = "ws://localhost:8000/rpc";

var allResults = new List<SurrealDbLiveQueryResponse>();

Func<Task> func = async () =>
{
await using var surrealDbClientGenerator = new SurrealDbClientGenerator();
var dbInfo = surrealDbClientGenerator.GenerateDatabaseInfo();

using var client = surrealDbClientGenerator.Create(url);
await client.SignIn(new RootAuth { Username = "root", Password = "root" });
await client.Use(dbInfo.Namespace, dbInfo.Database);

var response = await client.Query("LIVE SELECT * FROM test;");

if (response.FirstResult is not SurrealDbOkResult okResult)
throw new Exception("Expected a SurrealDbOkResult");

var liveQueryUuid = okResult.GetValue<Guid>();

var liveQuery = client.ListenLive<TestRecord>(liveQueryUuid);

var cts = new CancellationTokenSource();

_ = Task.Run(async () =>
{
await foreach (var result in liveQuery.WithCancellation(cts.Token))
{
allResults.Add(result);
}
});

_ = Task.Run(async () =>
{
await WaitLiveQueryCreationAsync();

var record = await client.Create("test", new TestRecord { Value = 1 });
await WaitLiveQueryNotificationAsync();

await client.Upsert(new TestRecord { Id = record.Id, Value = 2 });
await WaitLiveQueryNotificationAsync();

await client.Delete(record.Id!);
await WaitLiveQueryNotificationAsync();

await liveQuery.KillAsync();
await WaitLiveQueryNotificationAsync();

cts.Cancel();
});

await Task.Delay(Timeout);

if (!cts.IsCancellationRequested)
{
cts.Cancel();
throw new Exception("Timeout");
}
};

await func.Should().NotThrowAsync();

allResults.Should().HaveCount(4);

var firstResult = allResults[0];
firstResult.Should().BeOfType<SurrealDbLiveQueryCreateResponse<TestRecord>>();

var secondResult = allResults[1];
secondResult.Should().BeOfType<SurrealDbLiveQueryUpdateResponse<TestRecord>>();

var thirdResult = allResults[2];
thirdResult.Should().BeOfType<SurrealDbLiveQueryDeleteResponse>();

var lastResult = allResults[3];
lastResult.Should().BeOfType<SurrealDbLiveQueryCloseResponse>();
}
}
Loading

0 comments on commit 74d69b9

Please sign in to comment.