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..acc5466 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)); @@ -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-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..c39ea27 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. + /// 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..3e98191 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. + /// Decimal value to encode. /// 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..d790b18 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 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); + 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