From 977ffc13617fb6d39e68462db1c7e0e36694fe3b Mon Sep 17 00:00:00 2001
From: Nick Woolmer <29717167+nwoolmer@users.noreply.github.com>
Date: Mon, 17 Nov 2025 18:08:13 +0000
Subject: [PATCH 1/3] fix inference for null values
---
src/example-basic/Program.cs | 24 +-
src/net-questdb-client-tests/HttpTests.cs | 2 +-
.../JsonSpecTestRunner.cs | 23 +-
src/net-questdb-client-tests/TcpTests.cs | 306 +++++++++---------
src/net-questdb-client/Buffers/BufferV1.cs | 186 ++++++-----
src/net-questdb-client/Buffers/BufferV3.cs | 53 ++-
src/net-questdb-client/Buffers/IBuffer.cs | 22 +-
.../Senders/AbstractSender.cs | 47 +--
src/net-questdb-client/Senders/ISender.cs | 108 +++++--
9 files changed, 424 insertions(+), 347 deletions(-)
diff --git a/src/example-basic/Program.cs b/src/example-basic/Program.cs
index 99eb00c..5e0545c 100644
--- a/src/example-basic/Program.cs
+++ b/src/example-basic/Program.cs
@@ -1,19 +1,19 @@
using System;
using QuestDB;
-using var sender = Sender.New("http::addr=localhost:9000;");
+using var sender = Sender.New("http::addr=localhost:9000;protocol_version=3");
await sender.Table("trades")
- .Symbol("symbol", "ETH-USD")
- .Symbol("side", "sell")
- .Column("price", 2615.54)
- .Column("amount", 0.00044)
- .AtAsync(DateTime.UtcNow);
+ .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);
+ .Symbol("symbol", "BTC-USD")
+ .Symbol("side", "sell")
+ .Column("price", 39269.98)
+ .Column("amount", 0.001)
+ .AtAsync(DateTime.UtcNow);
-await sender.SendAsync();
+await sender.SendAsync();
\ No newline at end of file
diff --git a/src/net-questdb-client-tests/HttpTests.cs b/src/net-questdb-client-tests/HttpTests.cs
index 91ecaca..195dd6e 100644
--- a/src/net-questdb-client-tests/HttpTests.cs
+++ b/src/net-questdb-client-tests/HttpTests.cs
@@ -101,7 +101,7 @@ await sender.Table("metrics")
.Symbol("tag", "value")
.Column("dec_pos", 123.45m)
.Column("dec_neg", -123.45m)
- .Column("dec_null", (decimal?)null)
+ .NullableColumn("dec_null", (decimal?)null)
.Column("dec_max", decimal.MaxValue)
.Column("dec_min", decimal.MinValue)
.AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
diff --git a/src/net-questdb-client-tests/JsonSpecTestRunner.cs b/src/net-questdb-client-tests/JsonSpecTestRunner.cs
index f8093e9..ffa3104 100644
--- a/src/net-questdb-client-tests/JsonSpecTestRunner.cs
+++ b/src/net-questdb-client-tests/JsonSpecTestRunner.cs
@@ -45,7 +45,7 @@ public class JsonSpecTestRunner
private static readonly TestCase[]? TestCases = ReadTestCases();
///
- /// Populate the provided sender with the test case's table, symbols, and columns, then send the prepared row.
+ /// Populate the provided sender with the test case's table, symbols, and columns, then send the prepared row.
///
/// The ISender to configure and use for sending the test case row.
/// The test case containing table name, symbols, and typed columns to write.
@@ -82,13 +82,14 @@ private static async Task ExecuteTestCase(ISender sender, TestCase testCase)
var value = ((JsonElement)column.Value).GetString();
if (value is null)
{
- sender.Column(column.Name, (decimal?)null);
+ sender.NullableColumn(column.Name, (decimal?)null);
}
else
{
var d = decimal.Parse(value, NumberStyles.Number, CultureInfo.InvariantCulture);
sender.Column(column.Name, d);
}
+
break;
default:
@@ -103,9 +104,13 @@ private static async Task ExecuteTestCase(ISender sender, TestCase testCase)
}
///
- /// Executes the provided test case by sending its configured table, symbols, and columns to a local TCP listener and asserting the listener's received output against the test case's expected result.
+ /// Executes the provided test case by sending its configured table, symbols, and columns to a local TCP listener and
+ /// asserting the listener's received output against the test case's expected result.
///
- /// The test case to run; provides table, symbols, columns to send and a Result describing the expected validation (Status, Line, AnyLines, or BinaryBase64).
+ ///
+ /// The test case to run; provides table, symbols, columns to send and a Result describing the
+ /// expected validation (Status, Line, AnyLines, or BinaryBase64).
+ ///
[TestCaseSource(nameof(TestCases))]
public async Task RunTcp(TestCase testCase)
{
@@ -162,9 +167,13 @@ public async Task RunTcp(TestCase testCase)
}
///
- /// Executes the provided test case by sending data over HTTP to a dummy server using a QuestDB sender and validates the server's response according to the test case result.
+ /// Executes the provided test case by sending data over HTTP to a dummy server using a QuestDB sender and validates
+ /// the server's response according to the test case result.
///
- /// The test case describing table, symbols, columns, and expected result (status, line(s), or base64 binary) to execute and validate.
+ ///
+ /// The test case describing table, symbols, columns, and expected result (status, line(s), or
+ /// base64 binary) to execute and validate.
+ ///
[TestCaseSource(nameof(TestCases))]
public async Task RunHttp(TestCase testCase)
{
@@ -293,7 +302,7 @@ public class TestCase
[JsonPropertyName("result")] public TestCaseResult Result { get; set; } = null!;
///
- /// Provides the test case name for display and logging.
+ /// Provides the test case name for display and logging.
///
/// The TestName of the test case.
public override string ToString()
diff --git a/src/net-questdb-client-tests/TcpTests.cs b/src/net-questdb-client-tests/TcpTests.cs
index 7bbe2c6..9d4e4df 100644
--- a/src/net-questdb-client-tests/TcpTests.cs
+++ b/src/net-questdb-client-tests/TcpTests.cs
@@ -51,10 +51,10 @@ public async Task SendLine()
using var sender = Sender.New($"tcp::addr={_host}:{_port};");
await sender.Table("metric name")
- .Symbol("t a g", "v alu, e")
- .Column("number", 10)
- .Column("string", " -=\"")
- .AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
+ .Symbol("t a g", "v alu, e")
+ .Column("number", 10)
+ .Column("string", " -=\"")
+ .AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
await sender.SendAsync();
@@ -70,13 +70,13 @@ public async Task SendLineWithDecimalBinaryEncoding()
using var sender = Sender.New($"tcp::addr={_host}:{_port};protocol_version=3;");
await sender.Table("metrics")
- .Symbol("tag", "value")
- .Column("dec_pos", 123.45m)
- .Column("dec_neg", -123.45m)
- .Column("dec_null", (decimal?)null)
- .Column("dec_max", decimal.MaxValue)
- .Column("dec_min", decimal.MinValue)
- .AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
+ .Symbol("tag", "value")
+ .Column("dec_pos", 123.45m)
+ .Column("dec_neg", -123.45m)
+ .NullableColumn("dec_null", (decimal?)null)
+ .Column("dec_max", decimal.MaxValue)
+ .Column("dec_min", decimal.MinValue)
+ .AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
await sender.SendAsync();
@@ -115,14 +115,14 @@ public async Task SendLineWithArrayProtocolV2()
using var sender = Sender.New($"tcp::addr={_host}:{_port};protocol_version=2;");
await sender.Table("metric name")
- .Symbol("t a g", "v alu, e")
- .Column("number", 10)
- .Column("string", " -=\"")
- .Column("array", new[]
- {
- 1.2
- })
- .AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
+ .Symbol("t a g", "v alu, e")
+ .Column("number", 10)
+ .Column("string", " -=\"")
+ .Column("array", new[]
+ {
+ 1.2,
+ })
+ .AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
await sender.SendAsync();
@@ -142,13 +142,13 @@ public void SendLineWithArrayProtocolV1Exception()
try
{
sender.Table("metric name")
- .Symbol("t a g", "v alu, e")
- .Column("number", 10)
- .Column("string", " -=\"")
- .Column("array", new[]
- {
- 1.2
- });
+ .Symbol("t a g", "v alu, e")
+ .Column("number", 10)
+ .Column("string", " -=\"")
+ .Column("array", new[]
+ {
+ 1.2,
+ });
}
catch (IngressError err)
{
@@ -171,12 +171,12 @@ public async Task SendLineExceedsBuffer()
for (var i = 0; i < lineCount; i++)
{
await sender.Table("table name")
- .Symbol("t a g", "v alu, e")
- .Column("number", 10)
- .Column("db l", 123.12)
- .Column("string", " -=\"")
- .Column("при вед", "медвед")
- .AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
+ .Symbol("t a g", "v alu, e")
+ .Column("number", 10)
+ .Column("db l", 123.12)
+ .Column("string", " -=\"")
+ .Column("при вед", "медвед")
+ .AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
totalExpectedSb.Append(expected);
}
@@ -201,12 +201,12 @@ public async Task SendLineReusesBuffer()
for (var i = 0; i < lineCount; i++)
{
await sender.Table("table name")
- .Symbol("t a g", "v alu, e")
- .Column("number", 10)
- .Column("db l", 123.12)
- .Column("string", " -=\"")
- .Column("при вед", "медвед")
- .AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
+ .Symbol("t a g", "v alu, e")
+ .Column("number", 10)
+ .Column("db l", 123.12)
+ .Column("string", " -=\"")
+ .Column("при вед", "медвед")
+ .AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
totalExpectedSb.Append(expected);
}
@@ -215,12 +215,12 @@ await sender.Table("table name")
for (var i = 0; i < lineCount; i++)
{
await sender.Table("table name")
- .Symbol("t a g", "v alu, e")
- .Column("number", 10)
- .Column("db l", 123.12)
- .Column("string", " -=\"")
- .Column("при вед", "медвед")
- .AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
+ .Symbol("t a g", "v alu, e")
+ .Column("number", 10)
+ .Column("db l", 123.12)
+ .Column("string", " -=\"")
+ .Column("при вед", "медвед")
+ .AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
totalExpectedSb.Append(expected);
}
@@ -245,12 +245,12 @@ public async Task SendLineTrimsBuffers()
for (var i = 0; i < lineCount; i++)
{
await sender.Table("table name")
- .Symbol("t a g", "v alu, e")
- .Column("number", 10)
- .Column("db l", 123.12)
- .Column("string", " -=\"")
- .Column("при вед", "медвед")
- .AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
+ .Symbol("t a g", "v alu, e")
+ .Column("number", 10)
+ .Column("db l", 123.12)
+ .Column("string", " -=\"")
+ .Column("при вед", "медвед")
+ .AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
totalExpectedSb.Append(expected);
}
@@ -260,12 +260,12 @@ await sender.Table("table name")
for (var i = 0; i < lineCount; i++)
{
await sender.Table("table name")
- .Symbol("t a g", "v alu, e")
- .Column("number", 10)
- .Column("db l", 123.12)
- .Column("string", " -=\"")
- .Column("при вед", "медвед")
- .AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
+ .Symbol("t a g", "v alu, e")
+ .Column("number", 10)
+ .Column("db l", 123.12)
+ .Column("string", " -=\"")
+ .Column("при вед", "медвед")
+ .AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
totalExpectedSb.Append(expected);
}
@@ -291,12 +291,12 @@ public async Task ServerDisconnects()
for (var i = 0; i < lineCount; i++)
{
await sender.Table("table name")
- .Symbol("t a g", "v alu, e")
- .Column("number", 10)
- .Column("db l", 123.12)
- .Column("string", " -=\"")
- .Column("при вед", "медвед")
- .AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
+ .Symbol("t a g", "v alu, e")
+ .Column("number", 10)
+ .Column("db l", 123.12)
+ .Column("string", " -=\"")
+ .Column("при вед", "медвед")
+ .AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
totalExpectedSb.Append(expected);
try
{
@@ -325,11 +325,11 @@ public async Task SendNegativeLongAndDouble()
#pragma warning disable CS0618 // Type or member is obsolete
await sender.Table("neg name")
- .Column("number1", long.MinValue + 1)
- .Column("number2", long.MaxValue)
- .Column("number3", double.MinValue)
- .Column("number4", double.MaxValue)
- .AtNowAsync();
+ .Column("number1", long.MinValue + 1)
+ .Column("number2", long.MaxValue)
+ .Column("number3", double.MinValue)
+ .Column("number4", double.MaxValue)
+ .AtNowAsync();
#pragma warning restore CS0618 // Type or member is obsolete
await sender.SendAsync();
@@ -348,15 +348,15 @@ public async Task DoubleSerializationTest()
#pragma warning disable CS0618 // Type or member is obsolete
await sender.Table("doubles")
- .Column("d0", 0.0)
- .Column("dm0", -0.0)
- .Column("d1", 1.0)
- .Column("dE100", 1E100)
- .Column("d0000001", 0.000001)
- .Column("dNaN", double.NaN)
- .Column("dInf", double.PositiveInfinity)
- .Column("dNInf", double.NegativeInfinity)
- .AtNowAsync();
+ .Column("d0", 0.0)
+ .Column("dm0", -0.0)
+ .Column("d1", 1.0)
+ .Column("dE100", 1E100)
+ .Column("d0000001", 0.000001)
+ .Column("dNaN", double.NaN)
+ .Column("dInf", double.PositiveInfinity)
+ .Column("dNInf", double.NegativeInfinity)
+ .AtNowAsync();
#pragma warning restore CS0618 // Type or member is obsolete
await sender.SendAsync();
@@ -375,8 +375,8 @@ public async Task SendTimestampColumn()
var ts = new DateTime(2022, 2, 24);
await sender.Table("name")
- .Column("ts", ts)
- .AtAsync(ts);
+ .Column("ts", ts)
+ .AtAsync(ts);
await sender.SendAsync();
@@ -395,8 +395,8 @@ public async Task SendColumnNanos()
const long timestampNanos = 1645660800123456789L;
await sender.Table("name")
- .ColumnNanos("ts", timestampNanos)
- .AtAsync(timestampNanos);
+ .ColumnNanos("ts", timestampNanos)
+ .AtAsync(timestampNanos);
await sender.SendAsync();
@@ -415,8 +415,8 @@ public async Task SendAtNanos()
const long timestampNanos = 1645660800987654321L;
await sender.Table("name")
- .Column("value", 42)
- .AtNanosAsync(timestampNanos);
+ .Column("value", 42)
+ .AtNanosAsync(timestampNanos);
await sender.SendAsync();
@@ -431,8 +431,8 @@ public Task InvalidState()
using var srv = CreateTcpListener(_port);
srv.AcceptAsync();
- using var sender = Sender.New($"tcp::addr={_host}:{_port};");
- string? nullString = null;
+ using var sender = Sender.New($"tcp::addr={_host}:{_port};");
+ string? nullString = null;
Assert.That(
() => sender.Table(nullString),
@@ -534,8 +534,8 @@ public Task InvalidTableName()
using var srv = CreateTcpListener(_port);
srv.AcceptAsync();
- using var sender = Sender.New($"tcp::addr={_host}:{_port};");
- string? nullString = null;
+ using var sender = Sender.New($"tcp::addr={_host}:{_port};");
+ string? nullString = null;
Assert.Throws(() => sender.Table(nullString));
Assert.Throws(() => sender.Column("abc", 123));
@@ -562,22 +562,22 @@ public async Task CancelLine()
using var sender = Sender.New($"tcp::addr={_host}:{_port};");
await sender
- .Table("good")
- .Symbol("asdf", "sdfad")
- .Column("ddd", 123)
- .AtNowAsync();
+ .Table("good")
+ .Symbol("asdf", "sdfad")
+ .Column("ddd", 123)
+ .AtNowAsync();
await sender
- .Table("bad")
- .Symbol("asdf", "sdfad")
- .Column("asdf", 123)
- .AtAsync(DateTime.UtcNow);
+ .Table("bad")
+ .Symbol("asdf", "sdfad")
+ .Column("asdf", 123)
+ .AtAsync(DateTime.UtcNow);
sender.CancelRow();
await sender
- .Table("good")
- .AtAsync(new DateTime(1970, 1, 2));
+ .Table("good")
+ .AtAsync(new DateTime(1970, 1, 2));
await sender.SendAsync();
var expected = "good,asdf=sdfad ddd=123i\n" +
@@ -592,19 +592,19 @@ public async Task SendMillionAsyncExplicit()
srv.AcceptAsync();
var nowMillisecond = DateTime.Now.Millisecond;
- var metric = "metric_name" + nowMillisecond;
+ var metric = "metric_name" + nowMillisecond;
using var sender = Sender.New($"tcp::addr={_host}:{_port};init_buf_size={256 * 1024};");
for (var i = 0; i < 1E6; i++)
{
await sender.Table(metric)
- .Symbol("nopoint", "tag" + i % 100)
- .Column("counter", i * 1111.1)
- .Column("int", i)
- .Column("привед", "мед вед")
- .AtAsync(new DateTime(2021, 1, 1, i / 360 / 1000 % 60, i / 60 / 1000 % 60, i / 1000 % 60,
- i % 1000));
+ .Symbol("nopoint", "tag" + i % 100)
+ .Column("counter", i * 1111.1)
+ .Column("int", i)
+ .Column("привед", "мед вед")
+ .AtAsync(new DateTime(2021, 1, 1, i / 360 / 1000 % 60, i / 60 / 1000 % 60, i / 1000 % 60,
+ i % 1000));
if (i % 100 == 0)
{
@@ -622,7 +622,7 @@ public async Task SendMillionFixedBuffer()
srv.AcceptAsync();
var nowMillisecond = DateTime.Now.Millisecond;
- var metric = "metric_name" + nowMillisecond;
+ var metric = "metric_name" + nowMillisecond;
using var sender =
Sender.New(
@@ -631,12 +631,12 @@ public async Task SendMillionFixedBuffer()
for (var i = 0; i < 1E6; i++)
{
await sender.Table(metric)
- .Symbol("nopoint", "tag" + i % 100)
- .Column("counter", i * 1111.1)
- .Column("int", i)
- .Column("привед", "мед вед")
- .AtAsync(new DateTime(2021, 1, 1, i / 360 / 1000 % 60, i / 60 / 1000 % 60, i / 1000 % 60,
- i % 1000));
+ .Symbol("nopoint", "tag" + i % 100)
+ .Column("counter", i * 1111.1)
+ .Column("int", i)
+ .Column("привед", "мед вед")
+ .AtAsync(new DateTime(2021, 1, 1, i / 360 / 1000 % 60, i / 60 / 1000 % 60, i / 1000 % 60,
+ i % 1000));
}
await sender.SendAsync();
@@ -664,8 +664,8 @@ public Task SendNegativeLongMin()
Assert.That(
#pragma warning disable CS0618 // Type or member is obsolete
() => sender.Table("name")
- .Column("number1", long.MinValue)
- .AtNowAsync(),
+ .Column("number1", long.MinValue)
+ .AtNowAsync(),
#pragma warning restore CS0618 // Type or member is obsolete
Throws.TypeOf().With.Message.Contains("Special case")
);
@@ -683,8 +683,8 @@ public async Task SendSpecialStrings()
$"tcp::addr={_host}:{_port};");
#pragma warning disable CS0618 // Type or member is obsolete
await sender.Table("neg name")
- .Column("привед", " мед\rве\n д")
- .AtNowAsync();
+ .Column("привед", " мед\rве\n д")
+ .AtNowAsync();
#pragma warning restore CS0618 // Type or member is obsolete
await sender.SendAsync();
@@ -705,9 +705,9 @@ public Task SendTagAfterField()
Assert.That(
#pragma warning disable CS0618 // Type or member is obsolete
async () => await sender.Table("name")
- .Column("number1", 123)
- .Symbol("nand", "asdfa")
- .AtNowAsync(),
+ .Column("number1", 123)
+ .Symbol("nand", "asdfa")
+ .AtNowAsync(),
#pragma warning restore CS0618 // Type or member is obsolete
Throws.TypeOf()
);
@@ -727,9 +727,9 @@ public Task SendMetricOnce()
Assert.That(
#pragma warning disable CS0618 // Type or member is obsolete
async () => await sender.Table("name")
- .Column("number1", 123)
- .Table("nand")
- .AtNowAsync(),
+ .Column("number1", 123)
+ .Table("nand")
+ .AtNowAsync(),
#pragma warning restore CS0618 // Type or member is obsolete
Throws.TypeOf()
);
@@ -749,7 +749,7 @@ public Task StartFromMetric()
Assert.That(
#pragma warning disable CS0618 // Type or member is obsolete
async () => await sender.Column("number1", 123)
- .AtNowAsync(),
+ .AtNowAsync(),
#pragma warning restore CS0618 // Type or member is obsolete
Throws.TypeOf()
);
@@ -757,7 +757,7 @@ public Task StartFromMetric()
Assert.That(
#pragma warning disable CS0618 // Type or member is obsolete
async () => await sender.Symbol("number1", "1234")
- .AtNowAsync(),
+ .AtNowAsync(),
#pragma warning restore CS0618 // Type or member is obsolete
Throws.TypeOf()
);
@@ -870,39 +870,39 @@ public async Task SendVariousAts()
#pragma warning disable CS0618 // Type or member is obsolete
await sender.Table("foo")
- .Symbol("bah", "baz")
- .AtNowAsync();
+ .Symbol("bah", "baz")
+ .AtNowAsync();
#pragma warning restore CS0618 // Type or member is obsolete
await sender.Table("foo")
- .Symbol("bah", "baz")
- .AtAsync(DateTime.UtcNow);
+ .Symbol("bah", "baz")
+ .AtAsync(DateTime.UtcNow);
await sender.Table("foo")
- .Symbol("bah", "baz")
- .AtAsync(DateTimeOffset.UtcNow);
+ .Symbol("bah", "baz")
+ .AtAsync(DateTimeOffset.UtcNow);
await sender.Table("foo")
- .Symbol("bah", "baz")
- .AtAsync(DateTime.UtcNow.Ticks / 100);
+ .Symbol("bah", "baz")
+ .AtAsync(DateTime.UtcNow.Ticks / 100);
#pragma warning disable CS0618 // Type or member is obsolete
sender.Table("foo")
- .Symbol("bah", "baz")
- .AtNow();
+ .Symbol("bah", "baz")
+ .AtNow();
#pragma warning restore CS0618 // Type or member is obsolete
sender.Table("foo")
- .Symbol("bah", "baz")
- .At(DateTime.UtcNow);
+ .Symbol("bah", "baz")
+ .At(DateTime.UtcNow);
sender.Table("foo")
- .Symbol("bah", "baz")
- .At(DateTimeOffset.UtcNow);
+ .Symbol("bah", "baz")
+ .At(DateTimeOffset.UtcNow);
sender.Table("foo")
- .Symbol("bah", "baz")
- .At(DateTime.UtcNow.Ticks / 100);
+ .Symbol("bah", "baz")
+ .At(DateTime.UtcNow.Ticks / 100);
await sender.SendAsync();
}
@@ -930,7 +930,7 @@ public async Task Authenticate()
{
using var srv = CreateTcpListener(_port);
srv.WithAuth("testUser1", "Vs4e-cOLsVCntsMrZiAGAZtrkPXO00uoRLuA3d7gEcI=",
- "ANhR2AZSs4ar9urE5AZrJqu469X0r7gZ1BBEdcrAuL_6");
+ "ANhR2AZSs4ar9urE5AZrJqu469X0r7gZ1BBEdcrAuL_6");
srv.AcceptAsync();
using var sender =
@@ -938,10 +938,10 @@ public async Task Authenticate()
$"tcp::addr={_host}:{_port};username=testUser1;token=NgdiOWDoQNUP18WOnb1xkkEG5TzPYMda5SiUOvT1K0U=;");
await sender.Table("metric name")
- .Symbol("t a g", "v alu, e")
- .Column("number", 10)
- .Column("string", " -=\"")
- .AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
+ .Symbol("t a g", "v alu, e")
+ .Column("number", 10)
+ .Column("string", " -=\"")
+ .AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
await sender.SendAsync();
var expected = "metric\\ name,t\\ a\\ g=v\\ alu\\,\\ e number=10i,string=\" -=\\\"\" 1000000000\n";
@@ -953,14 +953,14 @@ public Task AuthFailWrongKid()
{
using var srv = CreateTcpListener(_port);
srv.WithAuth("testUser1", "Vs4e-cOLsVCntsMrZiAGAZtrkPXO00uoRLuA3d7gEcI=",
- "ANhR2AZSs4ar9urE5AZrJqu469X0r7gZ1BBEdcrAuL_6");
+ "ANhR2AZSs4ar9urE5AZrJqu469X0r7gZ1BBEdcrAuL_6");
srv.AcceptAsync();
Assert.That(
() => Sender.New($"tcp::addr={_host}:{_port};username=invalid;token=foo=;")
- ,
+ ,
Throws.TypeOf().With.InnerException.TypeOf().With.Message
- .Contains("Authentication failed")
+ .Contains("Authentication failed")
);
return Task.CompletedTask;
}
@@ -970,7 +970,7 @@ public Task AuthFailBadKey()
{
using var srv = CreateTcpListener(_port);
srv.WithAuth("testUser1", "Vs4e-cOLsVCntsMrZiAGAZtrkPXO00uoRLuA3d7gEcI=",
- "ANhR2AZSs4ar9urE5AZrJqu469X0r7gZ1BBEdcrAuL_6");
+ "ANhR2AZSs4ar9urE5AZrJqu469X0r7gZ1BBEdcrAuL_6");
srv.AcceptAsync();
using var sender = Sender.New(
@@ -982,10 +982,10 @@ public Task AuthFailBadKey()
for (var i = 0; i < 100; i++)
{
await sender.Table("metric name")
- .Symbol("t a g", "v alu, e")
- .Column("number", 10)
- .Column("string", " -=\"")
- .AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
+ .Symbol("t a g", "v alu, e")
+ .Column("number", 10)
+ .Column("string", " -=\"")
+ .AtAsync(new DateTime(1970, 01, 01, 0, 0, 1));
await sender.SendAsync();
Thread.Sleep(10);
}
@@ -993,7 +993,7 @@ await sender.Table("metric name")
Assert.Fail();
},
Throws.TypeOf().With.Message
- .Contains("Could not write data to server.")
+ .Contains("Could not write data to server.")
);
return Task.CompletedTask;
}
@@ -1002,7 +1002,7 @@ await sender.Table("metric name")
public void EcdsaSignatureLoop()
{
var privateKey = Convert.FromBase64String("NgdiOWDoQNUP18WOnb1xkkEG5TzPYMda5SiUOvT1K0U=");
- var p = SecNamedCurves.GetByName("secp256r1");
+ var p = SecNamedCurves.GetByName("secp256r1");
var parameters = new ECDomainParameters(p.Curve, p.G, p.N, p.H);
var m = new byte[512];
diff --git a/src/net-questdb-client/Buffers/BufferV1.cs b/src/net-questdb-client/Buffers/BufferV1.cs
index e4fc0fe..47a0a02 100644
--- a/src/net-questdb-client/Buffers/BufferV1.cs
+++ b/src/net-questdb-client/Buffers/BufferV1.cs
@@ -40,15 +40,15 @@ public class BufferV1 : IBuffer
private int _currentBufferIndex;
private string _currentTableName = null!;
private bool _hasTable;
- private int _lineStartLength;
private int _lineStartBufferIndex;
private int _lineStartBufferPosition;
+ private int _lineStartLength;
private bool _noFields = true;
private bool _noSymbols = true;
private bool _quoted;
///
- /// Initializes a new instance of BufferV1 for writing ILP (InfluxDB Line Protocol) messages.
+ /// Initializes a new instance of BufferV1 for writing ILP (InfluxDB Line Protocol) messages.
///
/// Initial size of each buffer chunk, in bytes.
/// Maximum allowed UTF-8 byte length for table and column names.
@@ -82,13 +82,13 @@ public IBuffer Transaction(ReadOnlySpan tableName)
if (WithinTransaction)
{
throw new IngressError(ErrorCode.InvalidApiCall,
- "Cannot start another transaction - only one allowed at a time.");
+ "Cannot start another transaction - only one allowed at a time.");
}
if (Length > 0)
{
throw new IngressError(ErrorCode.InvalidApiCall,
- "Buffer must be clear before you can start a transaction.");
+ "Buffer must be clear before you can start a transaction.");
}
GuardInvalidTableName(tableName);
@@ -139,28 +139,29 @@ public void AtNanos(long timestampNanos)
}
///
- /// Resets the buffer to its initial empty state and clears all written data.
+ /// Resets the buffer to its initial empty state and clears all written data.
///
///
- /// Clears lengths of all allocated chunks, resets the active chunk and write position,
- /// resets row and total-length counters, exits any transaction state, and clears the current table and line start markers.
+ /// Clears lengths of all allocated chunks, resets the active chunk and write position,
+ /// resets row and total-length counters, exits any transaction state, and clears the current table and line start
+ /// markers.
///
public void Clear()
{
_currentBufferIndex = 0;
- Chunk = _buffers[_currentBufferIndex].Buffer;
+ Chunk = _buffers[_currentBufferIndex].Buffer;
for (var i = 0; i < _buffers.Count; i++)
{
_buffers[i] = (_buffers[i].Buffer, 0);
}
- Position = 0;
- RowCount = 0;
- Length = 0;
- WithinTransaction = false;
- _currentTableName = "";
- _lineStartLength = 0;
- _lineStartBufferIndex = 0;
+ Position = 0;
+ RowCount = 0;
+ Length = 0;
+ WithinTransaction = false;
+ _currentTableName = "";
+ _lineStartLength = 0;
+ _lineStartBufferIndex = 0;
_lineStartBufferPosition = 0;
}
@@ -175,20 +176,19 @@ public void TrimExcessBuffers()
}
///
- /// Reverts the current (in-progress) row to its start position, removing any bytes written for that row.
+ /// Reverts the current (in-progress) row to its start position, removing any bytes written for that row.
///
///
- /// Restores the active buffer index, adjusts the total Length and current Position to the saved line start,
- /// and clears the table-set flag for the cancelled row.
+ /// Restores the active buffer index, adjusts the total Length and current Position to the saved line start,
+ /// and clears the table-set flag for the cancelled row.
///
public void CancelRow()
{
_currentBufferIndex = _lineStartBufferIndex;
- Chunk = _buffers[_currentBufferIndex].Buffer;
- Length = _lineStartLength;
- Position = _lineStartBufferPosition;
- _hasTable = false;
-
+ Chunk = _buffers[_currentBufferIndex].Buffer;
+ Length = _lineStartLength;
+ Position = _lineStartBufferPosition;
+ _hasTable = false;
}
///
@@ -245,13 +245,14 @@ public void WriteToStream(Stream stream, CancellationToken ct = default)
}
///
- /// Sets the table name for the current row and encodes it into the buffer, beginning a new line context.
+ /// Sets the table name for the current row and encodes it into the buffer, beginning a new line context.
///
/// The table name to write; must meet filesystem length limits and protocol naming rules.
/// This buffer instance to support fluent calls.
///
- /// Thrown with ErrorCode.InvalidApiCall if a transaction is active for a different table or if a table has already been set for the current line.
- /// Thrown with ErrorCode.InvalidName if the provided name violates length or character restrictions.
+ /// Thrown with ErrorCode.InvalidApiCall if a transaction is active for a different table or if a table has already
+ /// been set for the current line.
+ /// Thrown with ErrorCode.InvalidName if the provided name violates length or character restrictions.
///
public IBuffer Table(ReadOnlySpan name)
{
@@ -259,17 +260,17 @@ public IBuffer Table(ReadOnlySpan name)
if (WithinTransaction && name != _currentTableName)
{
throw new IngressError(ErrorCode.InvalidApiCall,
- "Transactions can only be for one table.");
+ "Transactions can only be for one table.");
}
GuardTableAlreadySet();
GuardInvalidTableName(name);
- _quoted = false;
+ _quoted = false;
_hasTable = true;
- _lineStartLength = Length;
- _lineStartBufferIndex = _currentBufferIndex;
+ _lineStartLength = Length;
+ _lineStartBufferIndex = _currentBufferIndex;
_lineStartBufferPosition = Position;
EncodeUtf8(name);
@@ -416,26 +417,29 @@ public void Put(ReadOnlySpan chars)
}
///
- /// Appends the decimal ASCII representation of the specified 64-bit integer to the buffer.
+ /// Appends the decimal ASCII representation of the specified 64-bit integer to the buffer.
///
/// The current buffer instance.
- /// Thrown when the value is , which cannot be represented by this method; the error contains an inner .
+ ///
+ /// Thrown when the value is , which cannot be represented by
+ /// this method; the error contains an inner .
+ ///
public IBuffer Put(long value)
{
if (value == long.MinValue)
{
throw new IngressError(ErrorCode.InvalidApiCall, "Special case, long.MinValue cannot be handled by QuestDB",
- new ArgumentOutOfRangeException());
+ new ArgumentOutOfRangeException());
}
- Span num = stackalloc byte[20];
- var pos = num.Length;
- var remaining = Math.Abs(value);
+ Span num = stackalloc byte[20];
+ var pos = num.Length;
+ var remaining = Math.Abs(value);
do
{
var digit = remaining % 10;
- num[--pos] = (byte)('0' + digit);
- remaining /= 10;
+ num[--pos] = (byte)('0' + digit);
+ remaining /= 10;
} while (remaining != 0);
if (value < 0)
@@ -448,7 +452,7 @@ public IBuffer Put(long value)
num.Slice(pos, len).CopyTo(Chunk.AsSpan(Position));
Position += len;
- Length += len;
+ Length += len;
return this;
}
@@ -486,23 +490,39 @@ public IBuffer Put(byte value)
return this;
}
+ ///
+ /// Attempts to add a DECIMAL column to the current row; DECIMAL types are not supported by Protocol Version V1.
+ ///
+ /// The column name to write.
+ /// The decimal value to write, or null to indicate absence.
+ /// The buffer instance for fluent chaining.
+ ///
+ /// Always thrown with to indicate DECIMAL is
+ /// unsupported.
+ ///
+ public virtual IBuffer Column(ReadOnlySpan name, decimal value)
+ {
+ throw new IngressError(ErrorCode.ProtocolVersionError, "Protocol Version does not support DECIMAL types");
+ }
+
///
- /// Advance the current buffer write position and the overall length by a given number of bytes.
+ /// Advance the current buffer write position and the overall length by a given number of bytes.
///
- /// The number of bytes to add to both and .
+ /// The number of bytes to add to both and .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void Advance(int by)
{
Position += by;
- Length += by;
+ Length += by;
}
///
- /// Sets the buffer's current table to the stored table name when a transaction is active and no table has been set for the current row.
+ /// Sets the buffer's current table to the stored table name when a transaction is active and no table has been set for
+ /// the current row.
///
///
- /// Has no effect if not within a transaction or if a table has already been set for the current row.
+ /// Has no effect if not within a transaction or if a table has already been set for the current row.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void SetTableIfAppropriate()
@@ -514,15 +534,16 @@ internal void SetTableIfAppropriate()
}
///
- /// Finalizes the current row: terminates it with a newline, increments the completed row counter, resets per-row flags, and enforces the buffer size limit.
+ /// Finalizes the current row: terminates it with a newline, increments the completed row counter, resets per-row
+ /// flags, and enforces the buffer size limit.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void FinishLine()
{
PutAscii('\n');
RowCount++;
- _hasTable = false;
- _noFields = true;
+ _hasTable = false;
+ _noFields = true;
_noSymbols = true;
GuardExceededMaxBufferSize();
}
@@ -537,16 +558,20 @@ private void GuardExceededMaxBufferSize()
if (Length > _maxBufSize)
{
throw new IngressError(ErrorCode.InvalidApiCall,
- $"Exceeded maximum buffer size. Current: {Length} Maximum: {_maxBufSize}");
+ $"Exceeded maximum buffer size. Current: {Length} Maximum: {_maxBufSize}");
}
}
///
- /// Writes the column name to the buffer and prepares for writing the column value by appending the appropriate separator and equals sign.
+ /// Writes the column name to the buffer and prepares for writing the column value by appending the appropriate
+ /// separator and equals sign.
///
/// The column name to write.
/// The buffer instance for fluent chaining.
- /// Thrown if the table is not set, the column name is invalid, or the name exceeds the maximum length.
+ ///
+ /// Thrown if the table is not set, the column name is invalid, or the name exceeds the
+ /// maximum length.
+ ///
internal IBuffer Column(ReadOnlySpan columnName)
{
GuardFsFileNameLimit(columnName);
@@ -567,22 +592,26 @@ internal IBuffer Column(ReadOnlySpan columnName)
}
///
- /// Validates that the requested additional byte count does not exceed the chunk size.
+ /// Validates that the requested additional byte count does not exceed the chunk size.
///
/// The number of additional bytes requested.
- /// Thrown with if the requested size exceeds the chunk length.
+ ///
+ /// Thrown with if the requested size exceeds the
+ /// chunk length.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void GuardAgainstOversizedChunk(int additional)
{
if (additional > Chunk.Length)
{
throw new IngressError(ErrorCode.InvalidApiCall,
- "tried to allocate oversized chunk: " + additional + " bytes");
+ "tried to allocate oversized chunk: " + additional + " bytes");
}
}
///
- /// Ensures that the current chunk has enough space to write the specified number of additional bytes; switches to the next buffer chunk if needed.
+ /// Ensures that the current chunk has enough space to write the specified number of additional bytes; switches to the
+ /// next buffer chunk if needed.
///
/// The number of additional bytes required.
/// Thrown if the requested size exceeds the chunk size.
@@ -597,7 +626,8 @@ internal void EnsureCapacity(int additional)
}
///
- /// Writes a non-ASCII character as UTF-8 to the buffer, switching to the next buffer chunk if insufficient space remains.
+ /// Writes a non-ASCII character as UTF-8 to the buffer, switching to the next buffer chunk if insufficient space
+ /// remains.
///
/// The character to encode and write.
private void PutUtf8(char c)
@@ -607,18 +637,19 @@ private void PutUtf8(char c)
NextBuffer();
}
- var bytes = Chunk.AsSpan(Position);
- Span chars = stackalloc char[1] { c, };
- var byteLength = Encoding.UTF8.GetBytes(chars, bytes);
+ var bytes = Chunk.AsSpan(Position);
+ Span chars = stackalloc char[1] { c, };
+ var byteLength = Encoding.UTF8.GetBytes(chars, bytes);
Advance(byteLength);
}
///
- /// Writes an ASCII character to the buffer, applying ILP escaping rules based on context (quoted or unquoted).
+ /// Writes an ASCII character to the buffer, applying ILP escaping rules based on context (quoted or unquoted).
///
/// The ASCII character to write.
///
- /// Escapes space, comma, equals, newline, carriage return, quote, and backslash characters according to ILP protocol requirements.
+ /// Escapes space, comma, equals, newline, carriage return, quote, and backslash characters according to ILP protocol
+ /// requirements.
///
private void PutSpecial(char c)
{
@@ -733,7 +764,7 @@ private static void GuardInvalidTableName(ReadOnlySpan tableName)
if (tableName.IsEmpty)
{
throw new IngressError(ErrorCode.InvalidName,
- "Table names must have a non-zero length.");
+ "Table names must have a non-zero length.");
}
var prev = '\0';
@@ -746,7 +777,7 @@ private static void GuardInvalidTableName(ReadOnlySpan tableName)
if (i == 0 || i == tableName.Length - 1 || prev == '.')
{
throw new IngressError(ErrorCode.InvalidName,
- $"Bad string {tableName}. Found invalid dot `.` at position {i}.");
+ $"Bad string {tableName}. Found invalid dot `.` at position {i}.");
}
break;
@@ -781,10 +812,10 @@ private static void GuardInvalidTableName(ReadOnlySpan tableName)
case '\x000f':
case '\x007f':
throw new IngressError(ErrorCode.InvalidName,
- $"Bad string {tableName}. Table names can't contain a {c} character, which was found at byte position {i}");
+ $"Bad string {tableName}. Table names can't contain a {c} character, which was found at byte position {i}");
case '\xfeff':
throw new IngressError(ErrorCode.InvalidName,
- $"Bad string {tableName}. Table names can't contain a UTF-8 BOM character, which was found at byte position {i}.");
+ $"Bad string {tableName}. Table names can't contain a UTF-8 BOM character, which was found at byte position {i}.");
}
prev = c;
@@ -801,7 +832,7 @@ private static void GuardInvalidColumnName(ReadOnlySpan columnName)
if (columnName.IsEmpty)
{
throw new IngressError(ErrorCode.InvalidName,
- "Column names must have a non-zero length.");
+ "Column names must have a non-zero length.");
}
for (var i = 0; i < columnName.Length; i++)
@@ -842,10 +873,10 @@ private static void GuardInvalidColumnName(ReadOnlySpan columnName)
case '\x000f':
case '\x007f':
throw new IngressError(ErrorCode.InvalidName,
- $"Bad string {columnName}. Column names can't contain a {c} character, which was found at byte position {i}");
+ $"Bad string {columnName}. Column names can't contain a {c} character, which was found at byte position {i}");
case '\xfeff':
throw new IngressError(ErrorCode.InvalidName,
- $"Bad string {columnName}. Column names can't contain a UTF-8 BOM character, which was found at byte position {i}.");
+ $"Bad string {columnName}. Column names can't contain a UTF-8 BOM character, which was found at byte position {i}.");
}
}
}
@@ -855,28 +886,19 @@ private static void GuardInvalidColumnName(ReadOnlySpan columnName)
///
///
///
- /// Validates that the UTF-8 encoded byte length of the given name is within the configured maximum.
+ /// Validates that the UTF-8 encoded byte length of the given name is within the configured maximum.
///
/// The name to validate (measured in UTF-8 bytes).
- /// Thrown with if the name exceeds the maximum allowed byte length.
+ ///
+ /// Thrown with if the name exceeds the maximum
+ /// allowed byte length.
+ ///
private void GuardFsFileNameLimit(ReadOnlySpan name)
{
if (Encoding.UTF8.GetBytes(name.ToString()).Length > _maxNameLen)
{
throw new IngressError(ErrorCode.InvalidApiCall,
- $"Name is too long, must be under {_maxNameLen} bytes.");
+ $"Name is too long, must be under {_maxNameLen} bytes.");
}
}
-
- ///
- /// Attempts to add a DECIMAL column to the current row; DECIMAL types are not supported by Protocol Version V1.
- ///
- /// The column name to write.
- /// The decimal value to write, or null to indicate absence.
- /// The buffer instance for fluent chaining.
- /// Always thrown with to indicate DECIMAL is unsupported.
- public virtual IBuffer Column(ReadOnlySpan name, decimal? value)
- {
- throw new IngressError(ErrorCode.ProtocolVersionError, "Protocol Version does not support DECIMAL types");
- }
}
\ No newline at end of file
diff --git a/src/net-questdb-client/Buffers/BufferV3.cs b/src/net-questdb-client/Buffers/BufferV3.cs
index d0e1a74..81151cc 100644
--- a/src/net-questdb-client/Buffers/BufferV3.cs
+++ b/src/net-questdb-client/Buffers/BufferV3.cs
@@ -30,16 +30,6 @@ namespace QuestDB.Buffers;
///
public class BufferV3 : BufferV2
{
- ///
- /// Initializes a new instance of BufferV3 with the specified buffer and name length limits.
- ///
- /// Initial size of the internal write buffer, in bytes.
- /// Maximum allowed length for column names, in characters.
- /// Maximum allowed internal buffer size, in bytes.
- public BufferV3(int bufferSize, int maxNameLen, int maxBufSize) : base(bufferSize, maxNameLen, maxBufSize)
- {
- }
-
// Sign mask for the flags field. A value of zero in this bit indicates a
// positive Decimal value, and a value of one in this bit indicates a
// negative Decimal value.
@@ -54,18 +44,24 @@ public BufferV3(int bufferSize, int maxNameLen, int maxBufSize) : base(bufferSiz
private const int ScaleShift = 16;
///
- /// Writes a decimal column in QuestDB's binary column format (scale, length, and two's-complement big-endian unscaled value).
+ /// Initializes a new instance of BufferV3 with the specified buffer and name length limits.
+ ///
+ /// Initial size of the internal write buffer, in bytes.
+ /// Maximum allowed length for column names, in characters.
+ /// Maximum allowed internal buffer size, in bytes.
+ public BufferV3(int bufferSize, int maxNameLen, int maxBufSize) : base(bufferSize, maxNameLen, maxBufSize)
+ {
+ }
+
+ ///
+ /// Writes a decimal column in QuestDB's binary column format (scale, length, and two's-complement big-endian unscaled
+ /// value).
///
/// Column name to write.
/// Nullable decimal value to encode; when null writes zero scale and zero length.
/// The buffer instance for call chaining.
- public override IBuffer Column(ReadOnlySpan name, decimal? value)
+ public override IBuffer Column(ReadOnlySpan name, decimal value)
{
- if (value is null)
- {
- return this;
- }
-
// # Binary Format
// 1. Binary format marker: `'='` (0x3D)
// 2. Type identifier: BinaryFormatType.DECIMAL byte
@@ -78,7 +74,7 @@ public override IBuffer Column(ReadOnlySpan name, decimal? value)
.Put((byte)BinaryFormatType.DECIMAL);
Span parts = stackalloc int[4];
- decimal.GetBits(value.Value, parts);
+ decimal.GetBits(value, parts);
var flags = parts[3];
var scale = (byte)((flags & ScaleMask) >> ScaleShift);
@@ -86,10 +82,10 @@ public override IBuffer Column(ReadOnlySpan name, decimal? value)
// 3. Scale
Put(scale);
- var low = parts[0];
- var mid = parts[1];
- var high = parts[2];
- var negative = (flags & SignMask) != 0 && value.Value != 0m;
+ var low = parts[0];
+ var mid = parts[1];
+ var high = parts[2];
+ var negative = (flags & SignMask) != 0 && value != 0m;
if (negative)
{
@@ -98,15 +94,15 @@ public override IBuffer Column(ReadOnlySpan name, decimal? value)
{
low = ~low + 1;
var c = low == 0 ? 1 : 0;
- mid = ~mid + c;
- c = mid == 0 && c == 1 ? 1 : 0;
+ mid = ~mid + c;
+ c = mid == 0 && c == 1 ? 1 : 0;
high = ~high + c;
}
}
// We write the byte array on the stack first so that we can compress (remove unnecessary bytes) it later.
- Span span = stackalloc byte[13];
- var signByte = (byte)(negative ? 255 : 0);
+ Span span = stackalloc byte[13];
+ var signByte = (byte)(negative ? 255 : 0);
span[0] = signByte;
BinaryPrimitives.WriteInt32BigEndian(span.Slice(1, 4), high);
BinaryPrimitives.WriteInt32BigEndian(span.Slice(5, 4), mid);
@@ -118,7 +114,10 @@ public override IBuffer Column(ReadOnlySpan name, decimal? value)
// We can strip prefix bits that are 0 (if positive) or 1 (if negative) as long as we keep at least
// one of it in front to convey the sign.
start < span.Length - 1 && span[start] == signByte && ((span[start + 1] ^ signByte) & 0x80) == 0;
- start++) ;
+ start++)
+ {
+ ;
+ }
// 4. Length
var size = span.Length - start;
diff --git a/src/net-questdb-client/Buffers/IBuffer.cs b/src/net-questdb-client/Buffers/IBuffer.cs
index b16b60f..541dd3c 100644
--- a/src/net-questdb-client/Buffers/IBuffer.cs
+++ b/src/net-questdb-client/Buffers/IBuffer.cs
@@ -255,41 +255,45 @@ public interface IBuffer
/// A span of value-type elements representing the column data.
/// The buffer instance for fluent chaining.
///
- /// This method requires protocol version 2 or later. It will throw an with if used with protocol version 1.
+ /// This method requires protocol version 2 or later. It will throw an with
+ /// if used with protocol version 1.
///
public IBuffer Column(ReadOnlySpan name, ReadOnlySpan value) where T : struct;
///
- /// Writes an array column value for the current row.
+ /// Writes an array column value for the current row.
///
/// The column name.
/// The array to write as the column value, or null to record a NULL value.
/// The same buffer instance for fluent chaining.
///
- /// This method requires protocol version 2 or later. It will throw an with if used with protocol version 1.
+ /// This method requires protocol version 2 or later. It will throw an with
+ /// if used with protocol version 1.
///
public IBuffer Column(ReadOnlySpan name, Array? value);
///
- /// Writes a column with the specified name using the provided enumerable of values and shape information.
+ /// Writes a column with the specified name using the provided enumerable of values and shape information.
///
/// The column name.
/// An enumerable of values for the column; elements are of the value type `T`.
/// An enumerable of integers describing the multidimensional shape/length(s) for the values.
- /// The same instance for call chaining.
+ /// The same instance for call chaining.
///
- /// This method requires protocol version 2 or later. It will throw an with if used with protocol version 1.
+ /// This method requires protocol version 2 or later. It will throw an with
+ /// if used with protocol version 1.
///
public IBuffer Column(ReadOnlySpan name, IEnumerable value, IEnumerable shape) where T : struct;
///
- /// Writes a DECIMAL column with the specified name using the ILP binary decimal layout.
+ /// Writes a DECIMAL column with the specified name using the ILP binary decimal layout.
///
/// The column name.
/// The decimal value to write, or `null` to write a NULL column.
/// The buffer instance for method chaining.
///
- /// This method requires protocol version 3 or later. It will throw an with if used with protocol version 1 or 2.
+ /// This method requires protocol version 3 or later. It will throw an with
+ /// if used with protocol version 1 or 2.
///
- public IBuffer Column(ReadOnlySpan name, decimal? value);
+ public IBuffer Column(ReadOnlySpan name, decimal value);
}
\ No newline at end of file
diff --git a/src/net-questdb-client/Senders/AbstractSender.cs b/src/net-questdb-client/Senders/AbstractSender.cs
index 27f74b6..b557c3c 100644
--- a/src/net-questdb-client/Senders/AbstractSender.cs
+++ b/src/net-questdb-client/Senders/AbstractSender.cs
@@ -95,11 +95,11 @@ public ISender Column(ReadOnlySpan name, long value)
}
///
- /// Appends an integer-valued column with the specified name to the current buffered row.
+ /// Appends an integer-valued column with the specified name to the current buffered row.
///
/// The column name.
/// The integer value to append for the column.
- /// The same instance to allow fluent chaining.
+ /// The same instance to allow fluent chaining.
public ISender Column(ReadOnlySpan name, int value)
{
Buffer.Column(name, value);
@@ -266,6 +266,18 @@ public void Clear()
///
public abstract void Send(CancellationToken ct = default);
+ ///
+ /// Adds a nullable decimal column value to the current row in the buffer.
+ ///
+ /// The column name.
+ /// The decimal value to write, or null to emit a null for the column.
+ /// The same instance for fluent chaining.
+ public ISender Column(ReadOnlySpan name, decimal value)
+ {
+ Buffer.Column(name, value);
+ return this;
+ }
+
public ISender Column(ReadOnlySpan name, T[] value) where T : struct
{
Buffer.Column(name, value);
@@ -304,17 +316,18 @@ private ValueTask FlushIfNecessaryAsync(CancellationToken ct = default)
}
///
- /// Synchronously checks auto-flush conditions and sends the buffer if thresholds are met.
+ /// Synchronously checks auto-flush conditions and sends the buffer if thresholds are met.
///
/// A user-provided cancellation token.
///
- /// Auto-flushing is triggered based on:
- ///
- /// - - the number of buffered ILP rows.
- /// - - the current length of the buffer in UTF-8 bytes.
- /// - - the elapsed time interval since the last flush.
- ///
- /// Has no effect within a transaction or if is set to .
+ /// Auto-flushing is triggered based on:
+ ///
+ /// - - the number of buffered ILP rows.
+ /// - - the current length of the buffer in UTF-8 bytes.
+ /// - - the elapsed time interval since the last flush.
+ ///
+ /// Has no effect within a transaction or if is set to
+ /// .
///
private void FlushIfNecessary(CancellationToken ct = default)
{
@@ -329,7 +342,7 @@ private void FlushIfNecessary(CancellationToken ct = default)
}
///
- /// Sets to the current UTC time if it has not been initialized.
+ /// Sets to the current UTC time if it has not been initialized.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void GuardLastFlushNotSet()
@@ -339,16 +352,4 @@ private void GuardLastFlushNotSet()
LastFlush = DateTime.UtcNow;
}
}
-
- ///
- /// Adds a nullable decimal column value to the current row in the buffer.
- ///
- /// The column name.
- /// The decimal value to write, or null to emit a null for the column.
- /// The same instance for fluent chaining.
- public ISender Column(ReadOnlySpan name, decimal? value)
- {
- Buffer.Column(name, value);
- return this;
- }
}
\ No newline at end of file
diff --git a/src/net-questdb-client/Senders/ISender.cs b/src/net-questdb-client/Senders/ISender.cs
index 3b117e7..c60e702 100644
--- a/src/net-questdb-client/Senders/ISender.cs
+++ b/src/net-questdb-client/Senders/ISender.cs
@@ -22,6 +22,7 @@
*
******************************************************************************/
+using QuestDB.Enums;
using QuestDB.Utils;
namespace QuestDB.Senders;
@@ -121,7 +122,7 @@ public interface ISender : IDisposable
/// The name of the column
/// The value for the column
///
- /// Adds a column (field) with the specified string value to the current row.
+ /// Adds a column (field) with the specified string value to the current row.
///
/// The column name.
/// The column value as a character span.
@@ -129,7 +130,7 @@ public interface ISender : IDisposable
public ISender Column(ReadOnlySpan name, ReadOnlySpan value);
///
- /// Adds a column with the specified name and 64-bit integer value to the current row.
+ /// Adds a column with the specified name and 64-bit integer value to the current row.
///
/// The column (field) name.
/// The 64-bit integer value for the column.
@@ -140,31 +141,31 @@ public interface ISender : IDisposable
public ISender Column(ReadOnlySpan name, int value);
///
- /// Adds a boolean field column with the specified name and value to the current row.
+ /// Adds a boolean field column with the specified name and value to the current row.
///
/// The column (field) name.
/// The boolean value to store in the column.
- /// The same instance to allow fluent chaining.
+ /// The same instance to allow fluent chaining.
public ISender Column(ReadOnlySpan name, bool value);
///
- /// Adds a double-precision field column to the current row.
+ /// Adds a double-precision field column to the current row.
///
/// The column (field) name.
/// The column's double-precision value.
- /// The same instance for fluent chaining.
+ /// The same instance for fluent chaining.
public ISender Column(ReadOnlySpan name, double value);
///
- /// Adds a column (field) with the specified DateTime value to the current row.
+ /// Adds a column (field) with the specified DateTime value to the current row.
///
/// The column name.
/// The DateTime value to add.
- /// The same instance for fluent chaining.
+ /// The same instance for fluent chaining.
public ISender Column(ReadOnlySpan name, DateTime value);
///
- /// Adds a column with the specified name and DateTimeOffset value to the current row.
+ /// Adds a column with the specified name and DateTimeOffset value to the current row.
///
/// The column name.
/// The DateTimeOffset value to store for the column (used as a timestamp value).
@@ -236,50 +237,61 @@ public interface ISender : IDisposable
public void CancelRow();
///
- /// Clears the sender's internal buffer and resets buffer-related state, removing all pending rows.
+ /// Clears the sender's internal buffer and resets buffer-related state, removing all pending rows.
///
public void Clear();
///
- /// Adds a column to the current row using a sequence of value-type elements and an explicit multidimensional shape.
+ /// Adds a column to the current row using a sequence of value-type elements and an explicit multidimensional shape.
///
/// The element value type stored in the column.
/// The column name.
/// A sequence of elements that form the column's data.
- /// A sequence of integers describing the dimensions of the array representation; dimension lengths must match the number of elements in when multiplied together.
- /// The same instance for fluent chaining.
+ ///
+ /// A sequence of integers describing the dimensions of the array representation; dimension lengths
+ /// must match the number of elements in when multiplied together.
+ ///
+ /// The same instance for fluent chaining.
public ISender Column(ReadOnlySpan name, IEnumerable value, IEnumerable shape) where T : struct;
///
- /// Adds a column whose value is provided as a native array; multidimensional (non-jagged) arrays are supported.
+ /// Adds a column whose value is provided as a native array; multidimensional (non-jagged) arrays are supported.
///
/// The column name.
- /// A native array containing the column data. Multidimensional arrays are treated as shaped data (do not pass jagged arrays).
+ ///
+ /// A native array containing the column data. Multidimensional arrays are treated as shaped data (do
+ /// not pass jagged arrays).
+ ///
/// The sender instance for fluent chaining.
public ISender Column(ReadOnlySpan name, Array value);
///
- /// Adds a column with the specified name and a sequence of value-type elements from a span to the current row.
+ /// Adds a column with the specified name and a sequence of value-type elements from a span to the current row.
///
/// The column (field) name.
/// A contiguous sequence of value-type elements representing the column data.
- /// The same instance to allow fluent chaining.
+ /// The same instance to allow fluent chaining.
public ISender Column(ReadOnlySpan name, ReadOnlySpan value) where T : struct;
///
- /// Adds a column with the specified string value to the current row.
+ /// Adds a column with the specified string value to the current row.
///
/// The column name.
/// The column's string value; may be null.
/// The same sender instance for fluent chaining.
public ISender Column(ReadOnlySpan name, string? value)
{
- if (value is null) return this;
+ if (value is null)
+ {
+ return this;
+ }
+
return Column(name, value.AsSpan());
}
///
- /// Adds a column whose value is a sequence of value-type elements with the given multidimensional shape when both and are provided; no action is taken if either is null.
+ /// Adds a column whose value is a sequence of value-type elements with the given multidimensional shape when both
+ /// and are provided; no action is taken if either is null.
///
/// The column name.
/// The sequence of elements for the column, or null to skip adding the column.
@@ -297,11 +309,14 @@ public ISender NullableColumn(ReadOnlySpan name, IEnumerable? value,
}
///
- /// Adds a column using a native array value when the provided array is non-null.
+ /// Adds a column using a native array value when the provided array is non-null.
///
/// The column name.
- /// The array to use as the column value; if null, no column is added. Multidimensional arrays are supported (non-jagged).
- /// The same instance for fluent chaining.
+ ///
+ /// The array to use as the column value; if null, no column is added. Multidimensional arrays are
+ /// supported (non-jagged).
+ ///
+ /// The same instance for fluent chaining.
public ISender NullableColumn(ReadOnlySpan name, Array? value)
{
if (value != null)
@@ -313,7 +328,7 @@ public ISender NullableColumn(ReadOnlySpan name, Array? value)
}
///
- /// Adds a string column with the given name when the provided value is not null.
+ /// Adds a string column with the given name when the provided value is not null.
///
/// The column name.
/// The string value to add; if null, no column is added.
@@ -329,7 +344,8 @@ public ISender NullableColumn(ReadOnlySpan name, string? value)
}
///
- /// Adds a long column with the specified name when the provided nullable value has a value; does nothing when the value is null.
+ /// Adds a long column with the specified name when the provided nullable value has a value; does nothing when the
+ /// value is null.
///
/// The column name.
/// The nullable long value to add as a column; if null the sender is unchanged.
@@ -345,7 +361,7 @@ public ISender NullableColumn(ReadOnlySpan name, long? value)
}
///
- /// Adds a boolean column with the given name when a value is provided; does nothing if the value is null.
+ /// Adds a boolean column with the given name when a value is provided; does nothing if the value is null.
///
/// The column name.
/// The nullable boolean value to add as a column.
@@ -361,7 +377,8 @@ public ISender NullableColumn(ReadOnlySpan name, bool? value)
}
///
- /// Adds a column with the given double value when the value is non-null; otherwise no column is added and the sender is unchanged.
+ /// Adds a column with the given double value when the value is non-null; otherwise no column is added and the sender
+ /// is unchanged.
///
/// The column name.
/// The column value; if non-null, the value is written as a double field.
@@ -377,11 +394,14 @@ public ISender NullableColumn(ReadOnlySpan name, double? value)
}
///
- /// Adds a DateTime column with the specified name when a value is provided; no action is taken if the value is null.
+ /// Adds a DateTime column with the specified name when a value is provided; no action is taken if the value is null.
///
/// The column name.
/// The nullable DateTime value to add as a column.
- /// The current instance for fluent chaining; unchanged if is null.
+ ///
+ /// The current instance for fluent chaining; unchanged if is
+ /// null.
+ ///
public ISender NullableColumn(ReadOnlySpan name, DateTime? value)
{
if (value != null)
@@ -393,11 +413,12 @@ public ISender NullableColumn(ReadOnlySpan name, DateTime? value)
}
///
- /// Adds a column with the given name and DateTimeOffset value when a value is provided; does nothing if the value is null.
+ /// Adds a column with the given name and DateTimeOffset value when a value is provided; does nothing if the value is
+ /// null.
///
/// The column name.
/// The DateTimeOffset value to add; if null the column is not added.
- /// The same instance to allow fluent chaining.
+ /// The same instance to allow fluent chaining.
public ISender NullableColumn(ReadOnlySpan name, DateTimeOffset? value)
{
if (value != null)
@@ -409,10 +430,31 @@ public ISender NullableColumn(ReadOnlySpan name, DateTimeOffset? value)
}
///
- /// Adds a decimal column in binary format to the current row.
+ /// Adds a decimal column in binary format to the current row.
///
/// The column name.
/// The decimal value to add; may be null to represent a NULL field.
/// The sender instance for fluent call chaining.
- public ISender Column(ReadOnlySpan name, decimal? value);
+ public ISender Column(ReadOnlySpan name, decimal value);
+
+
+ ///
+ /// Writes a DECIMAL column with the specified name using the ILP binary decimal layout.
+ ///
+ /// The column name.
+ /// The decimal value to write, or `null` to write a NULL column.
+ /// The buffer instance for method chaining.
+ ///
+ /// This method requires protocol version 3 or later. It will throw an with
+ /// if used with protocol version 1 or 2.
+ ///
+ public ISender NullableColumn(ReadOnlySpan name, decimal? value)
+ {
+ if (value != null)
+ {
+ return Column(name, value ?? throw new InvalidOperationException());
+ }
+
+ return this;
+ }
}
\ No newline at end of file
From 5165008b31b7ac08f5e00eeb02ef362579a99514 Mon Sep 17 00:00:00 2001
From: Nick Woolmer <29717167+nwoolmer@users.noreply.github.com>
Date: Mon, 17 Nov 2025 18:24:59 +0000
Subject: [PATCH 2/3] comments
---
src/net-questdb-client-tests/HttpTests.cs | 1 -
src/net-questdb-client/Buffers/BufferV1.cs | 2 +-
src/net-questdb-client/Senders/ISender.cs | 2 +-
3 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/net-questdb-client-tests/HttpTests.cs b/src/net-questdb-client-tests/HttpTests.cs
index 195dd6e..acc5466 100644
--- a/src/net-questdb-client-tests/HttpTests.cs
+++ b/src/net-questdb-client-tests/HttpTests.cs
@@ -1233,7 +1233,6 @@ public async Task CancelLineAfterError()
Assert.That(srv.PrintBuffer(), Is.EqualTo(expected));
}
-
[Test]
public async Task CannotConnect()
{
diff --git a/src/net-questdb-client/Buffers/BufferV1.cs b/src/net-questdb-client/Buffers/BufferV1.cs
index 47a0a02..c39ea27 100644
--- a/src/net-questdb-client/Buffers/BufferV1.cs
+++ b/src/net-questdb-client/Buffers/BufferV1.cs
@@ -494,7 +494,7 @@ public IBuffer Put(byte value)
/// Attempts to add a DECIMAL column to the current row; DECIMAL types are not supported by Protocol Version V1.
///
/// The column name to write.
- /// The decimal value to write, or null to indicate absence.
+ /// The decimal value to write.
/// The buffer instance for fluent chaining.
///
/// Always thrown with to indicate DECIMAL is
diff --git a/src/net-questdb-client/Senders/ISender.cs b/src/net-questdb-client/Senders/ISender.cs
index c60e702..d790b18 100644
--- a/src/net-questdb-client/Senders/ISender.cs
+++ b/src/net-questdb-client/Senders/ISender.cs
@@ -433,7 +433,7 @@ public ISender NullableColumn(ReadOnlySpan name, DateTimeOffset? value)
/// Adds a decimal column in binary format to the current row.
///
/// The column name.
- /// The decimal value to add; may be null to represent a NULL field.
+ /// The decimal value to add; if null is required, use the `NullableColumn` variant.
/// The sender instance for fluent call chaining.
public ISender Column(ReadOnlySpan name, decimal value);
From 85fe5c2e2872e3ef882eb6f7d71318033addeaf7 Mon Sep 17 00:00:00 2001
From: Nick Woolmer <29717167+nwoolmer@users.noreply.github.com>
Date: Mon, 17 Nov 2025 18:27:45 +0000
Subject: [PATCH 3/3] docstring
---
src/net-questdb-client/Buffers/BufferV3.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/net-questdb-client/Buffers/BufferV3.cs b/src/net-questdb-client/Buffers/BufferV3.cs
index 81151cc..3e98191 100644
--- a/src/net-questdb-client/Buffers/BufferV3.cs
+++ b/src/net-questdb-client/Buffers/BufferV3.cs
@@ -58,7 +58,7 @@ public BufferV3(int bufferSize, int maxNameLen, int maxBufSize) : base(bufferSiz
/// value).
///
/// Column name to write.
- /// Nullable decimal value to encode; when null writes zero scale and zero length.
+ /// Decimal value to encode.
/// The buffer instance for call chaining.
public override IBuffer Column(ReadOnlySpan name, decimal value)
{