Skip to content
Draft
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
7 changes: 4 additions & 3 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
<Authors>Harry Cordewener</Authors>
<Company>Harry Cordewener</Company>
<Copyright>Copyright © Harry Cordewener 2026</Copyright>
<!-- No repo-wide PackageLicenseExpression: every package published from this
repo bundles the BUSL-1.1 lora_ffi binaries alongside MIT managed code,
so each packable project sets PackageLicenseFile explicitly. -->
<!-- No repo-wide PackageLicenseExpression: LoraDb.Client is plain MIT, but
LoraDb.Client.Native bundles the BUSL-1.1 lora_ffi binaries alongside
MIT managed code and must use PackageLicenseFile instead. Each packable
project therefore declares its own license. -->
<PackageProjectUrl>https://github.com/HarryCordewener/loradb-dotnet-client</PackageProjectUrl>
<RepositoryUrl>https://github.com/HarryCordewener/loradb-dotnet-client</RepositoryUrl>
<RepositoryType>git</RepositoryType>
Expand Down
5 changes: 3 additions & 2 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ liblora_ffi.dylib). Those are compiled from https://github.com/lora-db/lora,
copyright LoraDB, Inc., and are licensed under the Business Source License 1.1.
Their license text is reproduced verbatim in THIRD-PARTY-NOTICES.md.

Both published NuGet packages bundle those binaries; PACKAGE-LICENSE.md is the
license file shipped inside the packages.
Of the two published NuGet packages, only LoraDb.Client.Native bundles those
binaries; PACKAGE-LICENSE.md is the license file shipped inside that package.
LoraDb.Client ships managed code only and is published as plain MIT.

----------------------------------------------------------------------------

Expand Down
52 changes: 52 additions & 0 deletions LoraDb.Client.Tests/NativeBridgeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,58 @@ public async Task Constructor_InvokesLibraryPathResolver()
}
}

[Test]
[NotInParallel("LibraryPathResolver")]
public async Task Constructor_PointsAtNativePackage_WhenDefaultLibraryIsMissing()
{
// A resolver that returns a path guarantees the load fails for a missing
// file regardless of what the OS search path happens to contain.
var missing = Path.Combine(Path.GetTempPath(), "loradb-absent-9f3c", "liblora_ffi.so");
var original = PInvokeLoraDbNativeBridge.LibraryPathResolver;
try
{
PInvokeLoraDbNativeBridge.LibraryPathResolver = _ => missing;

DllNotFoundException? caught = null;
try { _ = new PInvokeLoraDbNativeBridge("lora_ffi"); }
catch (DllNotFoundException ex) { caught = ex; }

await Assert.That(caught).IsNotNull();
await Assert.That(caught!.Message).Contains("LoraDb.Client.Native");
await Assert.That(caught.Message).Contains(missing);
await Assert.That(caught.InnerException).IsNotNull();
}
finally
{
PInvokeLoraDbNativeBridge.LibraryPathResolver = original;
}
}

[Test]
[NotInParallel("LibraryPathResolver")]
public async Task Constructor_OmitsNativePackageHint_ForCustomLibraryName()
{
// A caller who supplied their own library name is not missing the
// companion package, so the hint would be misleading.
var original = PInvokeLoraDbNativeBridge.LibraryPathResolver;
try
{
PInvokeLoraDbNativeBridge.LibraryPathResolver = _ => null;

DllNotFoundException? caught = null;
try { _ = new PInvokeLoraDbNativeBridge("custom_lora_build_9f3c"); }
catch (DllNotFoundException ex) { caught = ex; }

await Assert.That(caught).IsNotNull();
await Assert.That(caught!.Message).DoesNotContain("LoraDb.Client.Native");
await Assert.That(caught.Message).Contains("custom_lora_build_9f3c");
}
finally
{
PInvokeLoraDbNativeBridge.LibraryPathResolver = original;
}
}

[Test]
public async Task LibraryPathResolver_IsNotNull_AfterModuleInitializer()
{
Expand Down
32 changes: 9 additions & 23 deletions LoraDb.Client/LoraDb.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,26 @@
<PropertyGroup>
<TargetFrameworks>net10.0;netstandard2.1</TargetFrameworks>
<PackageId>LoraDb.Client</PackageId>
<Description>.NET client for LoraDB with HTTP and embedded Rust FFI transports. The managed client is MIT; this package also bundles lora_ffi native binaries from LoraDB, Inc. under Business Source License 1.1 — see PACKAGE-LICENSE.md and THIRD-PARTY-NOTICES.md in this package.</Description>
<!-- This package bundles the lora_ffi natives (see the runtimes ItemGroup
below), so it is not purely MIT. PackageLicenseExpression cannot express
the mix: it only accepts OSI/FSF-approved SPDX ids, and BUSL-1.1 is
neither. BUSL-1.1 also requires the license be conspicuously displayed
on every copy of the Licensed Work, so the full text ships in-package. -->
<PackageLicenseFile>PACKAGE-LICENSE.md</PackageLicenseFile>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<Description>.NET client for LoraDB with HTTP and embedded Rust FFI transports. HTTP mode works with this package alone; embedded mode additionally needs the LoraDb.Client.Native package, which supplies the lora_ffi binaries.</Description>
<!-- This package ships only managed code authored by this project, so a
plain MIT expression is accurate. The BUSL-1.1 lora_ffi binaries are
shipped exclusively by LoraDb.Client.Native. -->
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>

<ItemGroup>
<None Include="..\README.md" Pack="true" PackagePath="\" />
<None Include="..\PACKAGE-LICENSE.md" Pack="true" PackagePath="\" />
<None Include="..\THIRD-PARTY-NOTICES.md" Pack="true" PackagePath="\" />
</ItemGroup>

<!-- Source-linked (not binary-referenced): the resolver is MIT source, and
compiling it here registers the DllImport path hook when LoraDb.Client
itself loads. It only ever resolves binaries that some other package —
LoraDb.Client.Native — dropped into runtimes/{rid}/native/. -->
<ItemGroup>
<Compile Include="..\LoraDb.Client.Native\LoraDbNativeLoader.cs" Link="Native\LoraDbNativeLoader.cs"
Condition="'$(TargetFramework)' == 'net10.0'" />
</ItemGroup>

<ItemGroup>
<None Include="..\LoraDb.Client.Native\runtimes\win-x64\native\lora_ffi.dll"
Pack="true" PackagePath="runtimes/win-x64/native/" />
<None Include="..\LoraDb.Client.Native\runtimes\linux-x64\native\liblora_ffi.so"
Pack="true" PackagePath="runtimes/linux-x64/native/" />
<None Include="..\LoraDb.Client.Native\runtimes\linux-arm64\native\liblora_ffi.so"
Pack="true" PackagePath="runtimes/linux-arm64/native/" />
<None Include="..\LoraDb.Client.Native\runtimes\osx-x64\native\liblora_ffi.dylib"
Pack="true" PackagePath="runtimes/osx-x64/native/" />
<None Include="..\LoraDb.Client.Native\runtimes\osx-arm64\native\liblora_ffi.dylib"
Pack="true" PackagePath="runtimes/osx-arm64/native/" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.10" />
Expand Down
41 changes: 37 additions & 4 deletions LoraDb.Client/Native/PInvokeLoraDbNativeBridge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,16 @@ public void Dispose()
#else
/// <summary>
/// Optional resolver invoked before <see cref="NativeLibrary.Load(string)"/>.
/// Set by <c>LoraDb.Client.Native</c>'s module initializer to locate
/// RID-specific binaries shipped inside that NuGet package.
/// Set by the <c>LoraDbNativeLoader</c> module initializer to locate the
/// RID-specific binaries shipped in the <c>LoraDb.Client.Native</c> package.
/// Returns the full path to load, or <see langword="null"/> to fall back
/// to the OS default search.
/// </summary>
public static Func<string, string?>? LibraryPathResolver { get; set; }

/// <summary>Library name embedded mode loads unless overridden.</summary>
internal const string DefaultNativeLibraryName = "lora_ffi";

private IntPtr _libraryHandle;
private IntPtr _dbHandle;
private readonly object _sync = new();
Expand All @@ -54,7 +57,7 @@ public void Dispose()
private readonly DbSnapshotDelegate _dbLoadSnapshot;
private readonly FreeStringDelegate _freeString;

public PInvokeLoraDbNativeBridge(string libraryName = "lora_ffi")
public PInvokeLoraDbNativeBridge(string libraryName = DefaultNativeLibraryName)
: this(new LoraDbEmbeddedOpenOptions { NativeLibraryName = libraryName })
{
}
Expand All @@ -69,7 +72,15 @@ public PInvokeLoraDbNativeBridge(LoraDbEmbeddedOpenOptions openOptions)
throw new ArgumentException("DatabaseName and WalDirectory are mutually exclusive in embedded mode.", nameof(openOptions));

var resolvedPath = LibraryPathResolver?.Invoke(openOptions.NativeLibraryName) ?? openOptions.NativeLibraryName;
_libraryHandle = NativeLibrary.Load(resolvedPath);
try
{
_libraryHandle = NativeLibrary.Load(resolvedPath);
}
catch (DllNotFoundException ex)
{
throw new DllNotFoundException(
BuildLibraryNotFoundMessage(openOptions.NativeLibraryName, resolvedPath), ex);
}

_dbNew = Marshal.GetDelegateForFunctionPointer<DbNewDelegate>(
NativeLibrary.GetExport(_libraryHandle, "lora_db_new"));
Expand Down Expand Up @@ -306,6 +317,28 @@ private static (string Query, string? ParamsJson) ParseRequestJson(string reques
return (query, paramsJson);
}

/// <summary>
/// Builds an actionable message for a failed native load. The default
/// library name means the caller wanted embedded mode out of the box, and
/// the most common cause is that <c>LoraDb.Client.Native</c> — the package
/// that ships the binaries — was never installed.
/// </summary>
private static string BuildLibraryNotFoundMessage(string requestedName, string resolvedPath)
{
var detail = string.Equals(requestedName, resolvedPath, StringComparison.Ordinal)
? $"Unable to load native library '{requestedName}'."
: $"Unable to load native library '{requestedName}' (resolved to '{resolvedPath}').";

if (!requestedName.Equals(DefaultNativeLibraryName, StringComparison.OrdinalIgnoreCase))
return detail;

return detail
+ " Embedded mode needs the lora_ffi native binary, which the LoraDb.Client package does not ship."
+ " Install the companion package (dotnet add package LoraDb.Client.Native),"
+ " or set LoraDbEmbeddedOpenOptions.NativeLibraryName to the full path of your own build."
+ " HTTP mode does not need it.";
}

private static (string? ErrorCode, string Message) ParseError(string raw)
{
var delimiterIndex = raw.IndexOf(':');
Expand Down
4 changes: 3 additions & 1 deletion PACKAGE-LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# License

This package contains code under two different licenses.
This file ships inside the `LoraDb.Client.Native` package, which contains code
under two different licenses. (The companion `LoraDb.Client` package ships
managed code only and is published as plain MIT.)

## MIT — managed code authored by this project

Expand Down
29 changes: 22 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ A modern .NET client for [LoraDB](https://github.com/lora-db/lora) — a graph d
dotnet add package LoraDb.Client
```

That is everything HTTP mode needs. **Embedded mode additionally requires the
native binaries**, which ship in a separate package:

```bash
dotnet add package LoraDb.Client.Native
```

`LoraDb.Client.Native` is kept separate because the `lora_ffi` binaries are
BUSL-1.1 licensed — see [License](#-license). Without it, embedded mode fails at
startup with a `DllNotFoundException`.

---

## ⚡ Quick start
Expand Down Expand Up @@ -70,6 +81,9 @@ await using var client = LoraDbClient.CreateHttp(new Uri("http://127.0.0.1:4747/

### Embedded mode

> Requires the `LoraDb.Client.Native` package — or your own `lora_ffi` binary,
> pointed at via `LoraDbEmbeddedOpenOptions.NativeLibraryName`.

```csharp
// In-memory (ephemeral)
await using var client = LoraDbClient.CreateEmbedded();
Expand Down Expand Up @@ -153,7 +167,7 @@ Two licenses apply, depending on which part you use.
| Part | License |
|---|---|
| All C# source in this repo, and the managed assemblies in both packages | [MIT](LICENSE) — Copyright © 2026 Harry Cordewener |
| The bundled `lora_ffi` native libraries (`runtimes/*/native/`) | [Business Source License 1.1](THIRD-PARTY-NOTICES.md) — Copyright LoraDB, Inc. |
| The `lora_ffi` native libraries (`runtimes/*/native/`), bundled only in `LoraDb.Client.Native` | [Business Source License 1.1](THIRD-PARTY-NOTICES.md) — Copyright LoraDB, Inc. |

The native libraries are compiled from [LoraDB](https://github.com/lora-db/lora),
which is licensed under BUSL-1.1 (SPDX: `BUSL-1.1`), not an open source license.
Expand All @@ -162,11 +176,12 @@ Additional Use Grant permits internal-business and non-production use but does
not permit offering LoraDB as a database-as-a-service, hosted API, managed
database platform, or substantially similar hosted service for third parties.

**Both `LoraDb.Client` and `LoraDb.Client.Native` bundle these binaries**, so
the BUSL-1.1 terms apply to either package. Each package ships
`PACKAGE-LICENSE.md` (the split) and `THIRD-PARTY-NOTICES.md` (the verbatim
BUSL-1.1 text). Read them before use.
**Only `LoraDb.Client.Native` bundles these binaries**, so the BUSL-1.1 terms
apply to that package. It ships `PACKAGE-LICENSE.md` (the split) and
`THIRD-PARTY-NOTICES.md` (the verbatim BUSL-1.1 text) — read them before use.
`LoraDb.Client` contains managed code only and is published as plain MIT; if you
never enable embedded mode, BUSL-1.1 does not enter your dependency graph.

> Versions 0.1.2 and earlier of both packages incorrectly declared `MIT` as the
> sole NuGet license expression.
> Versions 0.1.2 and earlier of `LoraDb.Client` also bundled the binaries, and
> both packages incorrectly declared `MIT` as the sole NuGet license expression.

21 changes: 18 additions & 3 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,23 @@ await using var client = LoraDbClient.CreateHttp(endpoint, httpClientFactory);

### Embedded mode

Use embedded mode to execute queries through the native `lora_ffi` bridge:
Use embedded mode to execute queries through the native `lora_ffi` bridge.

Embedded mode needs the `lora_ffi` native library at runtime; `LoraDb.Client`
does not ship it. Add the companion package:

```bash
dotnet add package LoraDb.Client.Native
```

```csharp
using LoraDb.Client;

await using var client = LoraDbClient.CreateEmbedded();
```

> `LoraDb.Client.Native` bundles BUSL-1.1 licensed binaries — see the repository
> `LICENSE` and the `THIRD-PARTY-NOTICES.md` shipped in that package.
> Embedded mode is not supported on `netstandard2.1`.
> Embedded mode currently supports only `rows` query format.

Expand Down Expand Up @@ -138,9 +147,15 @@ Supported keys:

## 5) Embedded native library notes

By default, embedded mode loads library name `lora_ffi`.
By default, embedded mode loads library name `lora_ffi`. `LoraDb.Client`
registers a resolver that looks for the RID-specific binary under
`runtimes/{rid}/native/`, first next to the loaded assembly and then under
`AppContext.BaseDirectory` — that is where the `LoraDb.Client.Native` package
places the binaries. If neither is found the OS default search is used, and a
missing library surfaces as `DllNotFoundException`.

If needed, set a custom name via options:
If needed, set a custom name (or an absolute path to your own build) via
options:

```csharp
services.AddLoraDb(new LoraDbClientOptions
Expand Down