Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
008c5ed
refactor: move the example-aot project to the proper folder
RaphDal Oct 16, 2025
31f2972
feat: add BufferV3 and support for DECIMAL column values in IBuffer i…
RaphDal Oct 16, 2025
e509af7
feat: add support for decimal
RaphDal Oct 17, 2025
af2acea
Merge branch 'main' into rd_decimal
RaphDal Oct 17, 2025
16635e9
fix: update Buffer creation for ProtocolVersion.Auto to use BufferV3
RaphDal Oct 17, 2025
ea890c8
typo: fix typo in readme
RaphDal Oct 17, 2025
cfdb79e
📝 Add docstrings to `rd_decimal`
coderabbitai[bot] Oct 17, 2025
c9492d7
refactor: replace direct byte comparison with WaitAssert for improved…
RaphDal Oct 17, 2025
4850e7f
fix: handle null value in Column method of ISender interface
RaphDal Oct 17, 2025
5319a31
fix: handle null values in DECIMAL case of JsonSpecTestRunner
RaphDal Oct 17, 2025
e85fd36
refactor: remove unused using directive for System.Runtime.InteropSer…
RaphDal Oct 17, 2025
079195e
refactor: improve XML documentation for Buffer and IBuffer interfaces
RaphDal Oct 17, 2025
d9cd6d1
Merge pull request #53 from questdb/coderabbitai/docstrings/ea890c8
RaphDal Oct 17, 2025
92eadc4
fix: handle nullable decimal values in BufferV3
RaphDal Oct 17, 2025
6564091
fix: fix culture in dummy servers.
RaphDal Oct 17, 2025
d2b5163
refactor: enhance XML documentation across Buffer classes and senders
RaphDal Oct 17, 2025
3bf58c8
Update src/net-questdb-client/Senders/ISender.cs
RaphDal Oct 17, 2025
89d923c
Update src/net-questdb-client/Buffers/BufferV3.cs
RaphDal Oct 17, 2025
82a07fc
Update src/net-questdb-client/Senders/HttpSender.cs
RaphDal Oct 17, 2025
81db9f5
fix: correct error message wording for invalid table and column names
RaphDal Oct 17, 2025
c20de68
fix: ensure two's complement conversion is unchecked for negative man…
RaphDal Oct 17, 2025
b59e1ce
fix: improve buffer management in BufferV1 by resetting line start le…
RaphDal Oct 17, 2025
1be64bc
fix: update timestamp in TcpTests to use a specific date for consistency
RaphDal Oct 17, 2025
1495cb1
fix: update TcpTests to use AtNowAsync for current timestamp in test …
RaphDal Oct 20, 2025
bc20a0d
fix: don't write column when the value is null
RaphDal Oct 24, 2025
331083e
test: add BufferTests for decimal negation scenarios
RaphDal Oct 24, 2025
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
67 changes: 34 additions & 33 deletions README.md

Large diffs are not rendered by default.

8 changes: 1 addition & 7 deletions net-questdb-client.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ VisualStudioVersion = 16.0.30114.105
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "net-questdb-client", "src\net-questdb-client\net-questdb-client.csproj", "{456B1860-0102-48D7-861A-5F9963F3887B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "tcp-client-test", "src\tcp-client-test\tcp-client-test.csproj", "{22F903D9-4367-46A2-A25A-F4A6BF9105C6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "example-basic", "src\example-basic\example-basic.csproj", "{121EAA4D-3A73-468C-8CAB-A2A4BEF848CF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "example-auth-tls", "src\example-auth-tls\example-auth-tls.csproj", "{FBB8181C-6BAB-46C2-A47A-D3566A3997FE}"
Expand All @@ -21,7 +19,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "example-streaming", "src\ex
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "example-auth-http-tls", "src\example-auth-http-tls\example-auth-http-tls.csproj", "{24D93DBB-3783-423F-81CC-6B9BFD33F6CD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "example-aot", "example-aot\example-aot.csproj", "{5341FCF0-F71D-4160-8D6E-B5EFDF92E9E8}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "example-aot", "src\example-aot\example-aot.csproj", "{5341FCF0-F71D-4160-8D6E-B5EFDF92E9E8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -36,10 +34,6 @@ Global
{456B1860-0102-48D7-861A-5F9963F3887B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{456B1860-0102-48D7-861A-5F9963F3887B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{456B1860-0102-48D7-861A-5F9963F3887B}.Release|Any CPU.Build.0 = Release|Any CPU
{22F903D9-4367-46A2-A25A-F4A6BF9105C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{22F903D9-4367-46A2-A25A-F4A6BF9105C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{22F903D9-4367-46A2-A25A-F4A6BF9105C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{22F903D9-4367-46A2-A25A-F4A6BF9105C6}.Release|Any CPU.Build.0 = Release|Any CPU
{121EAA4D-3A73-468C-8CAB-A2A4BEF848CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{121EAA4D-3A73-468C-8CAB-A2A4BEF848CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{121EAA4D-3A73-468C-8CAB-A2A4BEF848CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down
70 changes: 56 additions & 14 deletions src/dummy-http-server/DummyHttpServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ public class DummyHttpServer : IDisposable
private int _port = 29743;
private readonly TimeSpan? _withStartDelay;

/// <summary>
/// Initializes a configurable in-process dummy HTTP server used for testing endpoints.
/// </summary>
/// <param name="withTokenAuth">If true, enable JWT bearer authentication and authorization.</param>
/// <param name="withBasicAuth">If true, enable basic authentication behavior in the test endpoint.</param>
/// <param name="withRetriableError">If true, configure the test endpoint to produce retriable error responses.</param>
/// <param name="withErrorMessage">If true, include error messages in test error responses.</param>
/// <param name="withStartDelay">Optional delay applied when starting the server.</param>
/// <param name="requireClientCert">If true, require client TLS certificates for HTTPS connections.</param>
public DummyHttpServer(bool withTokenAuth = false, bool withBasicAuth = false, bool withRetriableError = false,
bool withErrorMessage = false, TimeSpan? withStartDelay = null, bool requireClientCert = false)
{
Expand All @@ -54,10 +63,10 @@ public DummyHttpServer(bool withTokenAuth = false, bool withBasicAuth = false, b
.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)
Expand Down Expand Up @@ -108,26 +117,42 @@ public void Dispose()
_app.StopAsync().Wait();
}

/// <summary>
/// Clears the in-memory receive buffers and resets the endpoint error state and counter.
/// </summary>
/// <remarks>
/// Empties IlpEndpoint.ReceiveBuffer and IlpEndpoint.ReceiveBytes, sets IlpEndpoint.LastError to null,
/// and sets IlpEndpoint.Counter to zero.
/// </remarks>
public void Clear()
{
IlpEndpoint.ReceiveBuffer.Clear();
IlpEndpoint.ReceiveBytes.Clear();
IlpEndpoint.LastError = null;
IlpEndpoint.Counter = 0;
IlpEndpoint.Counter = 0;
}

/// <summary>
/// Starts the HTTP server on the specified port and configures the supported protocol versions.
/// </summary>
/// <param name="port">Port to listen on (defaults to 29743).</param>
/// <param name="versions">Array of supported protocol versions; defaults to {1, 2, 3} when null.</param>
/// <returns>A task that completes after any configured startup delay has elapsed and the server's background run task has been initiated.</returns>
public async Task StartAsync(int port = 29743, int[]? versions = null)
{
if (_withStartDelay.HasValue)
{
await Task.Delay(_withStartDelay.Value);
}
versions ??= new[] { 1, 2, };
SettingsEndpoint.Versions = versions;
_port = port;
_app.RunAsync($"http://localhost:{port}");
versions ??= new[] { 1, 2, 3, };
SettingsEndpoint.Versions = versions;
_port = port;
_ = _app.RunAsync($"http://localhost:{port}");
}

/// <summary>
/// Starts the web application and listens for HTTP requests on http://localhost:{_port}.
/// </summary>
public async Task RunAsync()
{
await _app.RunAsync($"http://localhost:{_port}");
Expand All @@ -138,12 +163,20 @@ public async Task StopAsync()
await _app.StopAsync();
}

/// <summary>
/// Gets the server's in-memory text buffer of received data.
/// </summary>
/// <returns>The mutable <see cref="StringBuilder"/> containing the accumulated received text; modifying it updates the server's buffer.</returns>
public StringBuilder GetReceiveBuffer()
{
return IlpEndpoint.ReceiveBuffer;
}

public List<byte> GetReceiveBytes()
/// <summary>
/// Gets the in-memory list of bytes received by the ILP endpoint.
/// </summary>
/// <returns>The mutable list of bytes received by the endpoint.</returns>
public List<byte> GetReceivedBytes()
{
return IlpEndpoint.ReceiveBytes;
}
Expand All @@ -160,14 +193,18 @@ public async Task<bool> Healthcheck()
}


/// <summary>
/// Generates a JWT for the test server when the provided credentials match the server's static username and password.
/// </summary>
/// <returns>The JWT string when credentials are valid; <c>null</c> otherwise. The issued token is valid for one day.</returns>
public string? GetJwtToken(string username, string password)
{
if (username == Username && password == Password)
{
var jwtToken = JwtBearer.CreateToken(o =>
{
o.SigningKey = SigningKey;
o.ExpireAt = DateTime.UtcNow.AddDays(1);
o.ExpireAt = DateTime.UtcNow.AddDays(1);
});
return jwtToken;
}
Expand All @@ -180,10 +217,15 @@ public int GetCounter()
return IlpEndpoint.Counter;
}

/// <summary>
/// Produces a human-readable string representation of the server's received-bytes buffer, interpreting embedded markers and formatting arrays and numeric values.
/// </summary>
/// <returns>The formatted textual representation of the received bytes buffer.</returns>
/// <exception cref="NotImplementedException">Thrown when the buffer contains an unsupported type code.</exception>
public string PrintBuffer()
{
var bytes = GetReceiveBytes().ToArray();
var sb = new StringBuilder();
var bytes = GetReceivedBytes().ToArray();
var sb = new StringBuilder();
var lastAppend = 0;

var i = 0;
Expand Down Expand Up @@ -252,7 +294,7 @@ public string PrintBuffer()
i--;
break;
default:
throw new NotImplementedException();
throw new NotImplementedException($"Type {bytes[i]} not implemented");
}

lastAppend = i + 1;
Expand All @@ -263,4 +305,4 @@ public string PrintBuffer()
sb.Append(Encoding.UTF8.GetString(bytes, lastAppend, i - lastAppend));
return sb.ToString();
}
}
}
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</PropertyGroup>

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

</Project>
2 changes: 1 addition & 1 deletion src/net-questdb-client-benchmarks/BenchInserts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
using BenchmarkDotNet.Attributes;
using dummy_http_server;
using QuestDB;
using tcp_client_test;
using net_questdb_client_tests;

#pragma warning disable CS0414 // Field is assigned but its value is never used

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@

<ItemGroup>
<ProjectReference Include="..\dummy-http-server\dummy-http-server.csproj"/>
<ProjectReference Include="..\net-questdb-client-tests\net-questdb-client-tests.csproj" />
<ProjectReference Include="..\net-questdb-client\net-questdb-client.csproj"/>
<ProjectReference Include="..\tcp-client-test\tcp-client-test.csproj"/>
</ItemGroup>

<ItemGroup>
Expand Down
92 changes: 92 additions & 0 deletions src/net-questdb-client-tests/DecimalTestHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*******************************************************************************
* ___ _ ____ ____
* / _ \ _ _ ___ ___| |_| _ \| __ )
* | | | | | | |/ _ \/ __| __| | | | _ \
* | |_| | |_| | __/\__ \ |_| |_| | |_) |
* \__\_\\__,_|\___||___/\__|____/|____/
*
* Copyright (c) 2014-2019 Appsicle
* Copyright (c) 2019-2024 QuestDB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************/

using System.Text;
using NUnit.Framework;
using QuestDB.Enums;

namespace net_questdb_client_tests;

internal static class DecimalTestHelpers
{
/// <summary>
/// Asserts that the buffer contains a decimal field for the specified column with the given scale and mantissa bytes.
/// </summary>
/// <param name="buffer">The encoded row payload to search for the column's decimal field.</param>
/// <param name="columnName">The name of the column whose decimal payload is expected in the buffer.</param>
/// <param name="expectedScale">The expected scale byte of the decimal field.</param>
/// <param name="expectedMantissa">The expected mantissa bytes of the decimal field.</param>
internal static void AssertDecimalField(ReadOnlySpan<byte> buffer,
string columnName,
byte expectedScale,
ReadOnlySpan<byte> expectedMantissa)
{
var payload = ExtractDecimalPayload(buffer, columnName);
Assert.That(payload.Length,
Is.GreaterThanOrEqualTo(4 + expectedMantissa.Length),
$"Decimal field `{columnName}` payload shorter than expected.");
Assert.That(payload[0], Is.EqualTo((byte)'='));
Assert.That(payload[1], Is.EqualTo((byte)BinaryFormatType.DECIMAL));
Assert.That(payload[2], Is.EqualTo(expectedScale), $"Unexpected scale for `{columnName}`.");
Assert.That(payload[3],
Is.EqualTo((byte)expectedMantissa.Length),
$"Unexpected mantissa length for `{columnName}`.");
CollectionAssert.AreEqual(expectedMantissa.ToArray(), payload.Slice(4, expectedMantissa.Length).ToArray(),
$"Mantissa bytes for `{columnName}` did not match expectation.");
}

/// <summary>
/// Asserts that the buffer contains a null decimal field payload for the specified column.
/// </summary>
/// <param name="buffer">Buffer containing the encoded record(s) to inspect.</param>
/// <param name="columnName">Name of the column whose decimal payload should be null.</param>
/// <remarks>
/// Verifies the payload starts with '=' then the DECIMAL type marker, and that both scale and mantissa length are zero.
/// </remarks>
internal static void AssertDecimalNullField(ReadOnlySpan<byte> buffer, string columnName)
{
var payload = ExtractDecimalPayload(buffer, columnName);
Assert.That(payload.Length,
Is.GreaterThanOrEqualTo(4),
$"Decimal field `{columnName}` payload shorter than expected.");
Assert.That(payload[0], Is.EqualTo((byte)'='));
Assert.That(payload[1], Is.EqualTo((byte)BinaryFormatType.DECIMAL));
Assert.That(payload[2], Is.EqualTo(0), $"Unexpected scale for `{columnName}`.");
Assert.That(payload[3], Is.EqualTo(0), $"Unexpected mantissa length for `{columnName}`.");
}

/// <summary>
/// Locate and return the payload bytes for a decimal column identified by name.
/// </summary>
/// <param name="buffer">The byte span containing the encoded record payload to search.</param>
/// <param name="columnName">The column name whose payload prefix ("columnName=") will be located.</param>
/// <returns>The slice of <paramref name="buffer"/> immediately after the found "columnName=" prefix.</returns>
private static ReadOnlySpan<byte> ExtractDecimalPayload(ReadOnlySpan<byte> buffer, string columnName)
{
var prefix = Encoding.ASCII.GetBytes($"{columnName}=");
var index = buffer.IndexOf(prefix.AsSpan());
Assert.That(index, Is.GreaterThanOrEqualTo(0), $"Column `{columnName}` not found in payload.");
return buffer[(index + prefix.Length)..];
}
}
Loading