Skip to content

Commit

Permalink
✨ implement Export method
Browse files Browse the repository at this point in the history
  • Loading branch information
Odonno committed Dec 8, 2024
1 parent 7b59c83 commit bccb378
Show file tree
Hide file tree
Showing 24 changed files with 593 additions and 31 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,9 @@ FodyWeavers.xsd
*.sln.iml
.idea/

# Verify snaspshot testing
*.received.*

### VisualStudio Patch ###
# Additional files built by Visual Studio

Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<PackageVersion Include="System.Interactive.Async" Version="6.0.1" />
<PackageVersion Include="SystemTextJsonPatch" Version="4.0.0" />
<PackageVersion Include="Ulid" Version="1.3.3" />
<PackageVersion Include="Verify.Xunit" Version="28.2.1" />
<PackageVersion Include="Websocket.Client" Version="5.1.1" />
<PackageVersion Include="xunit" Version="2.9.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
Expand Down
8 changes: 8 additions & 0 deletions SurrealDb.Embedded.InMemory/NativeMethods.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ internal static unsafe partial class NativeMethods
[DllImport(__DllName, EntryPoint = "execute", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void execute(int id, Method method, byte* bytes, int len, SuccessAction success, FailureAction failure);

/// <summary>
/// # Safety
///
/// Executes the "export" method of a SurrealDB engine (given its id).
/// </summary>
[DllImport(__DllName, EntryPoint = "export", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void export(int id, byte* bytes, int len, SuccessAction success, FailureAction failure);

/// <summary>
/// # Safety
///
Expand Down
105 changes: 105 additions & 0 deletions SurrealDb.Embedded.Internals/SurrealDbEmbeddedEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,111 @@ public ValueTask DisposeAsync()
#endif
}

public async Task<string> Export(ExportOptions? options, CancellationToken cancellationToken)
{
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
cancellationToken.Register(timeoutCts.Cancel);

await using var stream = MemoryStreamProvider.MemoryStreamManager.GetStream();

try
{
await CborSerializer
.SerializeAsync(options ?? new(), stream, GetCborOptions(), timeoutCts.Token)
.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
if (!cancellationToken.IsCancellationRequested)
{
throw new TimeoutException();
}

throw;
}

bool canGetBuffer = stream.TryGetBuffer(out var bytes);
if (!canGetBuffer)
{
throw new SurrealDbException("Failed to retrieve serialized buffer.");
}

var taskCompletionSource = new TaskCompletionSource<string>();
timeoutCts.Token.Register(() =>
{
taskCompletionSource.TrySetCanceled();
});

Action<ByteBuffer> success = (byteBuffer) =>
{
try
{
var result = CborSerializer.Deserialize<string>(
byteBuffer.AsReadOnly(),
GetCborOptions()
);
taskCompletionSource.SetResult(result!);
}
catch (Exception e)
{
taskCompletionSource.SetException(e);
}
};
Action<ByteBuffer> fail = (byteBuffer) =>
{
string error = CborSerializer.Deserialize<string>(
byteBuffer.AsReadOnly(),
GetCborOptions()
);
taskCompletionSource.SetException(new SurrealDbException(error));
};

var successHandle = GCHandle.Alloc(success);
var failureHandle = GCHandle.Alloc(fail);

unsafe
{
var successAction = new SuccessAction()
{
handle = new RustGCHandle()
{
ptr = GCHandle.ToIntPtr(successHandle),
drop_callback = &NativeBindings.DropGcHandle
},
callback = &NativeBindings.SuccessCallback,
};

var failureAction = new FailureAction()
{
handle = new RustGCHandle()
{
ptr = GCHandle.ToIntPtr(failureHandle),
drop_callback = &NativeBindings.DropGcHandle
},
callback = &NativeBindings.FailureCallback,
};

fixed (byte* payload = bytes.AsSpan())
{
NativeMethods.export(_id, payload, bytes.Count, successAction, failureAction);
}
}

try
{
return await taskCompletionSource.Task.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
if (!cancellationToken.IsCancellationRequested)
{
throw new TimeoutException();
}

throw;
}
}

public async Task<bool> Health(CancellationToken cancellationToken)
{
try
Expand Down
8 changes: 8 additions & 0 deletions SurrealDb.Embedded.RocksDb/NativeMethods.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ internal static unsafe partial class NativeMethods
[DllImport(__DllName, EntryPoint = "execute", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void execute(int id, Method method, byte* bytes, int len, SuccessAction success, FailureAction failure);

/// <summary>
/// # Safety
///
/// Executes the "export" method of a SurrealDB engine (given its id).
/// </summary>
[DllImport(__DllName, EntryPoint = "export", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void export(int id, byte* bytes, int len, SuccessAction success, FailureAction failure);

/// <summary>
/// # Safety
///
Expand Down
8 changes: 8 additions & 0 deletions SurrealDb.Embedded.SurrealKv/NativeMethods.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ internal static unsafe partial class NativeMethods
[DllImport(__DllName, EntryPoint = "execute", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void execute(int id, Method method, byte* bytes, int len, SuccessAction success, FailureAction failure);

/// <summary>
/// # Safety
///
/// Executes the "export" method of a SurrealDB engine (given its id).
/// </summary>
[DllImport(__DllName, EntryPoint = "export", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern void export(int id, byte* bytes, int len, SuccessAction success, FailureAction failure);

/// <summary>
/// # Safety
///
Expand Down
115 changes: 115 additions & 0 deletions SurrealDb.Net.Tests/ExportTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using System.Text;
using System.Text.RegularExpressions;

namespace SurrealDb.Net.Tests;

public class ExportTests
{
private readonly VerifySettings _verifySettings = new();

public ExportTests()
{
_verifySettings.DisableRequireUniquePrefix();
_verifySettings.UseDirectory("Snapshots");

// 💡 "ScrubInlineDateTimes" won't work as DateTime cannot be parsed with more than 7 seconds fraction units
_verifySettings.AddScrubber(
(sb, counter) =>
{
const string pattern = @"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3,9}Z";
var matches = Regex.Matches(sb.ToString(), pattern);

foreach (Match match in matches)
{
string value = match.Value;

var date = DateTime.Parse(value[..^4] + "Z");
int id = counter.Next(date);

string name = $"DateTime_{id}";

sb.Replace(value, name);
}
}
);
_verifySettings.AddScrubber(
(sb, _) =>
{
// 💡 Ensures line return doesn't breaking Snapshot testing
sb.Replace("\n\n\n\n", "\n\n");
}
);
}

[Theory]
[InlineData("Endpoint=mem://")]
[InlineData("Endpoint=rocksdb://")]
[InlineData("Endpoint=surrealkv://")]
[InlineData("Endpoint=http://127.0.0.1:8000;User=root;Pass=root")]
[InlineData("Endpoint=ws://127.0.0.1:8000/rpc;User=root;Pass=root")]
public async Task ShouldExportEmptyDatabaseWithDefaultOptions(string connectionString)
{
string? result = null;

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

await using var client = surrealDbClientGenerator.Create(connectionString);
await client.Use(dbInfo.Namespace, dbInfo.Database);

result = await client.Export();
};

await func.Should().NotThrowAsync();

await Verify(result, _verifySettings);
}

[Theory]
[InlineData("Endpoint=mem://")]
[InlineData("Endpoint=rocksdb://")]
[InlineData("Endpoint=surrealkv://")]
[InlineData("Endpoint=http://127.0.0.1:8000;User=root;Pass=root")]
[InlineData("Endpoint=ws://127.0.0.1:8000/rpc;User=root;Pass=root")]
public async Task ShouldExportFullDatabaseWithDefaultOptions(string connectionString)
{
string? result = null;

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

string filePath = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
"Schemas/post.surql"
);
string fileContent = await File.ReadAllTextAsync(filePath, Encoding.UTF8);

string query = fileContent;

await using var client = surrealDbClientGenerator.Create(connectionString);
await client.Use(dbInfo.Namespace, dbInfo.Database);
(await client.RawQuery(query)).EnsureAllOks();

await client.Delete("post");

var post = new Post
{
Id = ("post", "dotnet-123456"),
Title = "A new article",
Content = "This is a new article created using the .NET SDK"
};

await client.Create(post);

result = await client.Export();
};

await func.Should().NotThrowAsync();

await Verify(result, _verifySettings);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- ------------------------------
-- OPTION
-- ------------------------------

OPTION IMPORT;

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-- ------------------------------
-- OPTION
-- ------------------------------

OPTION IMPORT;

-- ------------------------------
-- TABLE: post
-- ------------------------------

DEFINE TABLE post TYPE NORMAL SCHEMAFULL PERMISSIONS FOR select, create, update, delete WHERE $auth.id != NONE;

DEFINE FIELD content ON post TYPE string PERMISSIONS FULL;
DEFINE FIELD created_at ON post TYPE datetime DEFAULT time::now() PERMISSIONS FULL;
DEFINE FIELD status ON post TYPE string DEFAULT 'DRAFT' ASSERT $value INSIDE ['DRAFT', 'PUBLISHED'] PERMISSIONS FULL;
DEFINE FIELD title ON post TYPE string PERMISSIONS FULL;

-- ------------------------------
-- TABLE DATA: post
-- ------------------------------

INSERT [ { content: 'This is a new article created using the .NET SDK', created_at: d'DateTime_1', id: post:⟨dotnet-123456⟩, status: 'DRAFT', title: 'A new article' } ];

1 change: 1 addition & 0 deletions SurrealDb.Net.Tests/SurrealDb.Net.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Semver" />
<PackageReference Include="Verify.Xunit" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
Loading

0 comments on commit bccb378

Please sign in to comment.