HoloDb client

HoloDb over the network: the client and the wire protocol

HoloDb.Client connects your .NET app to a HoloDb server over TCP with mandatory TLS and token authentication. HoloDb.Protocol is the columnar wire protocol between the two.

The client

Run SQL queries and bulk-load data with the same API as the embedded, in-process HoloDbService, so you can move from embedded to networked by changing only how you create the handle. Building a data-driven app usually means choosing between speed (embed the database) and scale (run it on its own server), with two APIs that behave differently. The client gives you one code path for both.

Install it with dotnet add package EvaluatedApplications.HoloDb.Client. It targets .NET 8 and .NET 10 and needs a HoloDb server it can reach over TLS.

Same API, embedded or networked
Execute, ExecuteAsync, BulkLoadAsync, GetStatsAsync and PingAsync have the same surface whether you talk to the in-process service or a remote server.
Columnar over the wire
Results are typed column arrays that match the in-process engine's QueryResult. There is no row buffering and no JSON serialization.
Secure by default
TLS is on. Trust a self-signed server by pinning its certificate thumbprint, or skip it in development only.
Resilience when you opt in
Exponential backoff with full jitter, a circuit breaker and a per-operation deadline budget across retries.
Built for concurrency
One client per concurrent worker, or pool clients up front with HoloDbClientPool.

Connect and query

These examples are compiled against the published package. Running them needs a HoloDb server.

Connect and run a query
using HoloDb.Client;

// Connect to a server (compile-checked here: running it needs a HoloDb server)
var client = await HoloDbClient.ConnectAsync("db.example.com", 5433);
try
{
    // The same surface as the in-process HoloDbService
    var result = await client.ExecuteAsync("SELECT id, value FROM data LIMIT 10");
    foreach (var row in result.Rows)
        Console.WriteLine($"id={row["id"]}, value={row["value"]}");
}
finally
{
    await client.DisposeAsync();
}
Retry and circuit breaker
using HoloDb.Client;

// Wraps the client with exponential backoff and a circuit breaker (compile-checked)
var resilient = await ResilientHoloDbClient.ConnectAsync(
    "db.example.com", 5433,
    retryDelay: TimeSpan.FromMilliseconds(200),
    maxAttempts: 3,
    breakerFailureThreshold: 5);

// Retries transport failures; SQL and authentication errors are not retried
var result = await resilient.ExecuteAsync("SELECT COUNT(*) FROM data");
Console.WriteLine(result.RowCount);
Pin the server certificate
using HoloDb.Client;

// Pin the server certificate by thumbprint, for a self-hosted server (compile-checked)
var options = new HoloDbClientOptions
{
    AuthToken = "your-token",
    UseTls = true,
    PinnedThumbprint = "ABC123DEF456",
};
var client = await HoloDbClient.ConnectAsync("localhost", 5433, options);
await client.DisposeAsync();

Things to know

One client is one session
Requests serialize through an internal queue, and concurrent callers share the same TCP connection. Use one client per worker, or a pool.
No automatic reconnect
A dropped socket raises an exception, and you reconnect explicitly. ResilientHoloDbClient does this for you when you use it.
Transient and application errors differ
HoloRemoteException (SQL errors, authentication failures) is not retried. The resilient client retries only transport failures such as IOException and SocketException.
Timeouts
ConnectAsync takes a TimeSpan for the connection only. The resilient client adds an operation timeout to every method, a budget across all its retries.

The wire protocol

HoloDb.Protocol defines how the server and client communicate. Instead of sending results as JSON or row objects it keeps the columnar shape: a long[], double[] or string[] travels as a typed array. That keeps the engine's columnar advantage intact when data moves between processes. You normally get the package through HoloDb.Client. Depend on it directly only to implement a server.

The wire format is stable: opcode byte values never change, and field order and types are fixed. Clients and servers from different builds negotiate a version and fail cleanly when incompatible.

Length-prefixed TCP framing
Safe message boundaries, with frame size limits and count bounds checks that stop a hostile length prefix from forcing a huge allocation.
Binary columnar codec
Typed long, double and string arrays in little-endian format. Non-columnar results (joins, GROUP BY) are packed in a compact binary row format.
Request and response dispatch
Opcodes for Hello (handshake), Exec (SQL), BulkLoad, Stats and Ping.
Error semantics
Protocol errors (corrupt or malformed bytes) are distinct from query errors (bad SQL, a constraint violation), so a caller knows whether to retry.
Transport-agnostic
It works over TCP, TLS or any duplex Stream. The protocol provides no encryption: TLS is the transport layer's job, and the authentication token travels in the Hello handshake as UTF-8, so run it over TLS.

A columnar query returns column arrays, not rows. If you expect row-by-row iteration, read the columns by position as above, or use Rows to materialise dictionaries.

Read a columnar result without row unpacking
using HoloDb.Client;

// Reading a columnar result without row unpacking (compile-checked here; it needs a server to run)
await using var client = await HoloDbClient.ConnectAsync("localhost", 5433, new HoloDbClientOptions { AuthToken = "mytoken" });
var result = await client.ExecuteAsync("SELECT id, price FROM prices");

if (result.IsColumnar)
{
    // column 0 is a long, column 1 is a double
    for (int r = 0; r < result.RowCount; r++)
        Console.WriteLine($"ID {result.GetLong(r, 0)}: price {result.GetDouble(r, 1)}");
}
One request in a server loop
using HoloDb;
using HoloDb.Protocol;

// A server loop built on the wire protocol (compile-checked here; it needs a connected stream to run)
async Task HandleOneRequest(Stream networkStream, HoloDbService engine, CancellationToken ct)
{
    var payload = await Wire.ReadFrameAsync(networkStream, ct);
    var request = HoloProtocol.DecodeRequest(payload);

    if (request.Op == WireOp.Exec)
    {
        var result = await engine.ExecuteAsync(request.Sql);
        var response = HoloProtocol.Result(result);
        await Wire.WriteFrameAsync(networkStream, response, ct);
    }
}
Console.WriteLine(nameof(HandleOneRequest));