Skip to content

feat: support for binary DOUBLE and ARRAY[DOUBLE] #38

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

Merged
merged 14 commits into from
Jul 14, 2025
Merged
22 changes: 22 additions & 0 deletions example-aot/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using QuestDB;

using var sender = Sender.New("http::addr=localhost:9000;");

await sender.Table("trades")
.Symbol("symbol", "ETH-USD")
.Symbol("side", "sell")
.Column("price", 2615.54)
.Column("amount", 0.00044)
.AtAsync(DateTime.UtcNow);

await sender.Table("trades")
.Symbol("symbol", "BTC-USD")
.Symbol("side", "sell")
.Column("price", 39269.98)
.Column("amount", 0.001)
.AtAsync(DateTime.UtcNow);

await sender.SendAsync();

// Test with:
// dotnet publish -r osx-arm64 -c Release
17 changes: 17 additions & 0 deletions example-aot/example-aot.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>example_aot</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\src\net-questdb-client\net-questdb-client.csproj"/>
</ItemGroup>

</Project>
6 changes: 6 additions & 0 deletions net-questdb-client.sln
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "net-questdb-client-tcp-auth
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "net-questdb-client-tcp-auth-test", "src\net-questdb-client-tcp-auth-tests\net-questdb-client-tcp-auth-tests.csproj", "{628A6AE1-C0D4-4A40-98DF-1F094BD60203}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "example-aot", "example-aot\example-aot.csproj", "{5341FCF0-F71D-4160-8D6E-B5EFDF92E9E8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -78,5 +80,9 @@ Global
{628A6AE1-C0D4-4A40-98DF-1F094BD60203}.Debug|Any CPU.Build.0 = Debug|Any CPU
{628A6AE1-C0D4-4A40-98DF-1F094BD60203}.Release|Any CPU.ActiveCfg = Release|Any CPU
{628A6AE1-C0D4-4A40-98DF-1F094BD60203}.Release|Any CPU.Build.0 = Release|Any CPU
{5341FCF0-F71D-4160-8D6E-B5EFDF92E9E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5341FCF0-F71D-4160-8D6E-B5EFDF92E9E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5341FCF0-F71D-4160-8D6E-B5EFDF92E9E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5341FCF0-F71D-4160-8D6E-B5EFDF92E9E8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
144 changes: 118 additions & 26 deletions src/dummy-http-server/DummyHttpServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,11 @@
******************************************************************************/


using System;
using System.Net.Http;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using FastEndpoints;
using FastEndpoints.Security;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace dummy_http_server;

Expand All @@ -41,30 +36,32 @@ public class DummyHttpServer : IDisposable
private static readonly string SigningKey = Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N");
private static readonly string Username = "admin";
private static readonly string Password = "quest";
private int _port = 29743;
private readonly WebApplication _app;
private int _port = 29743;
private readonly TimeSpan? _withStartDelay;

public DummyHttpServer(bool withTokenAuth = false, bool withBasicAuth = false, bool withRetriableError=false, bool withErrorMessage = false)
public DummyHttpServer(bool withTokenAuth = false, bool withBasicAuth = false, bool withRetriableError = false,
bool withErrorMessage = false, TimeSpan? withStartDelay = null)
{
var bld = WebApplication.CreateBuilder();

bld.Services.AddLogging(
builder =>
{
builder.AddFilter("Microsoft", LogLevel.Warning)
.AddFilter("System", LogLevel.Warning)
.AddConsole();
});
bld.Services.AddLogging(builder =>
{
builder.AddFilter("Microsoft", LogLevel.Warning)
.AddFilter("System", LogLevel.Warning)
.AddConsole();
});

IlpEndpoint.WithTokenAuth = withTokenAuth;
IlpEndpoint.WithBasicAuth = withBasicAuth;
IlpEndpoint.WithTokenAuth = withTokenAuth;
IlpEndpoint.WithBasicAuth = withBasicAuth;
IlpEndpoint.WithRetriableError = withRetriableError;
IlpEndpoint.WithErrorMessage = withErrorMessage;
IlpEndpoint.WithErrorMessage = withErrorMessage;
_withStartDelay = withStartDelay;

if (withTokenAuth)
{
bld.Services.AddAuthenticationJwtBearer(s => s.SigningKey = SigningKey)
.AddAuthorization();
.AddAuthorization();
}


Expand All @@ -75,7 +72,7 @@ public DummyHttpServer(bool withTokenAuth = false, bool withBasicAuth = false, b
{
o.Limits.MaxRequestBodySize = 1073741824;
o.ListenLocalhost(29474,
options => { options.UseHttps(); });
options => { options.UseHttps(); });
o.ListenLocalhost(29473);
});

Expand Down Expand Up @@ -103,15 +100,21 @@ public void Dispose()
public void Clear()
{
IlpEndpoint.ReceiveBuffer.Clear();
IlpEndpoint.ReceiveBytes.Clear();
IlpEndpoint.LastError = null;
IlpEndpoint.Counter = 0;
IlpEndpoint.Counter = 0;
}

public Task StartAsync(int port = 29743)
public async Task StartAsync(int port = 29743, int[]? versions = null)
{
_port = port;
if (_withStartDelay.HasValue)
{
await Task.Delay(_withStartDelay.Value);
}
versions ??= new[] { 1, 2, };
SettingsEndpoint.Versions = versions;
_port = port;
_app.RunAsync($"http://localhost:{port}");
return Task.CompletedTask;
}

public async Task RunAsync()
Expand All @@ -129,6 +132,11 @@ public StringBuilder GetReceiveBuffer()
return IlpEndpoint.ReceiveBuffer;
}

public List<byte> GetReceiveBytes()
{
return IlpEndpoint.ReceiveBytes;
}

public Exception? GetLastError()
{
return IlpEndpoint.LastError;
Expand All @@ -148,7 +156,7 @@ public async Task<bool> Healthcheck()
var jwtToken = JwtBearer.CreateToken(o =>
{
o.SigningKey = SigningKey;
o.ExpireAt = DateTime.UtcNow.AddDays(1);
o.ExpireAt = DateTime.UtcNow.AddDays(1);
});
return jwtToken;
}
Expand All @@ -160,4 +168,88 @@ public int GetCounter()
{
return IlpEndpoint.Counter;
}

public string PrintBuffer()
{
var bytes = GetReceiveBytes().ToArray();
var sb = new StringBuilder();
var lastAppend = 0;

var i = 0;
for (; i < bytes.Length; i++)
{
if (bytes[i] == (byte)'=')
{
if (bytes[i - 1] == (byte)'=')
{
sb.Append(Encoding.UTF8.GetString(bytes, lastAppend, i + 1 - lastAppend));
switch (bytes[++i])
{
case 14:
sb.Append("ARRAY<");
var type = bytes[++i];

Debug.Assert(type == 10);
var dims = bytes[++i];

++i;

long length = 0;
for (var j = 0; j < dims; j++)
{
var lengthBytes = bytes.AsSpan()[i..(i + 4)];
var lengthValue = MemoryMarshal.Cast<byte, uint>(lengthBytes)[0];
if (length == 0)
{
length = lengthValue;
}
else
{
length *= lengthValue;
}

sb.Append(lengthValue);
sb.Append(',');
i += 4;
}

sb.Remove(sb.Length - 1, 1);
sb.Append('>');

var doubleBytes =
MemoryMarshal.Cast<byte, double>(bytes.AsSpan().Slice(i, (int)(length * 8)));


sb.Append('[');
for (var j = 0; j < length; j++)
{
sb.Append(doubleBytes[j]);
sb.Append(',');
}

sb.Remove(sb.Length - 1, 1);
sb.Append(']');

i += (int)(length * 8);
i--;
break;
case 16:
sb.Remove(sb.Length - 1, 1);
var doubleValue = MemoryMarshal.Cast<byte, double>(bytes.AsSpan().Slice(++i, 8));
sb.Append(doubleValue[0]);
i += 8;
i--;
break;
default:
throw new NotImplementedException();
}

lastAppend = i + 1;
}
}
}

sb.Append(Encoding.UTF8.GetString(bytes, lastAppend, i - lastAppend));
return sb.ToString();
}
}
49 changes: 34 additions & 15 deletions src/dummy-http-server/IlpEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,46 +24,63 @@
******************************************************************************/


using System;
using System.Linq;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using FastEndpoints;

// ReSharper disable ClassNeverInstantiated.Global
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.

namespace dummy_http_server;

public record Request : IPlainTextRequest
public record Request
{
public string Content { get; set; }
public byte[] ByteContent { get; init; }
public string StringContent { get; init; }
}

[SuppressMessage("ReSharper", "InconsistentNaming")]
public record JsonErrorResponse
{
public string code { get; init; }
public string message { get; init; }
public int line { get; init; }
public string errorId { get; init; }

public override string ToString()
{
return $"\nServer Response (\n\tCode: `{code}`\n\tMessage: `{message}`\n\tLine: `{line}`\n\tErrorId: `{errorId}` \n)";
return
$"\nServer Response (\n\tCode: `{code}`\n\tMessage: `{message}`\n\tLine: `{line}`\n\tErrorId: `{errorId}` \n)";
}
}

public class Binder : IRequestBinder<Request>
{
public async ValueTask<Request> BindAsync(BinderContext ctx, CancellationToken ct)
{
// populate and return a request dto object however you please...
var ms = new MemoryStream();
await ctx.HttpContext.Request.Body.CopyToAsync(ms, ct);
return new Request
{
ByteContent = ms.ToArray(),
StringContent = Encoding.UTF8.GetString(ms.ToArray()),
};
}
}

public class IlpEndpoint : Endpoint<Request, JsonErrorResponse?>
{
private const string Username = "admin";
private const string Password = "quest";
public static readonly StringBuilder ReceiveBuffer = new();
public static readonly List<byte> ReceiveBytes = new();
public static Exception? LastError = new();
public static bool WithTokenAuth = false;
public static bool WithBasicAuth = false;
public static bool WithRetriableError = false;
public static bool WithErrorMessage = false;
private const string Username = "admin";
private const string Password = "quest";
public static int Counter = 0;
public static int Counter;

public override void Configure()
{
Expand All @@ -79,6 +96,7 @@ public override void Configure()
}

Description(b => b.Accepts<Request>());
RequestBinder(new Binder());
}

public override async Task HandleAsync(Request req, CancellationToken ct)
Expand All @@ -92,14 +110,15 @@ public override async Task HandleAsync(Request req, CancellationToken ct)

if (WithErrorMessage)
{
await SendAsync(new JsonErrorResponse()
{ code = "code", errorId = "errorid", line = 1, message = "message" }, 400, ct);
await SendAsync(new JsonErrorResponse
{ code = "code", errorId = "errorid", line = 1, message = "message", }, 400, ct);
return;
}

try
{
ReceiveBuffer.Append(req.Content);
ReceiveBuffer.Append(req.StringContent);
ReceiveBytes.AddRange(req.ByteContent);
await SendNoContentAsync(ct);
}
catch (Exception ex)
Expand Down
Loading