From 65e9b7bd7da90b9509be67835cd6e39f45e3058a Mon Sep 17 00:00:00 2001 From: victor Date: Wed, 3 Sep 2025 22:09:11 +0800 Subject: [PATCH 1/4] timestamp column use nanos precision. --- src/net-questdb-client-tests/HttpTests.cs | 4 ++-- src/net-questdb-client-tests/TcpTests.cs | 2 +- src/net-questdb-client/Buffers/BufferV1.cs | 2 +- src/tcp-client-test/LineTcpSenderTests.cs | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/net-questdb-client-tests/HttpTests.cs b/src/net-questdb-client-tests/HttpTests.cs index 6a2c21c..4f6917d 100644 --- a/src/net-questdb-client-tests/HttpTests.cs +++ b/src/net-questdb-client-tests/HttpTests.cs @@ -747,7 +747,7 @@ await sender.Table("name") await sender.SendAsync(); var expected = - "name ts=1645660800000000t 1645660800000000000\n"; + "name ts=1645660800000000000n 1645660800000000000\n"; Assert.That(srv.PrintBuffer(), Is.EqualTo(expected)); } @@ -1163,7 +1163,7 @@ public async Task TransactionMultipleTypes() await sender.CommitAsync(); var expected = - "tableName,foo=bah 86400000000000\ntableName foo=123i 86400000000000\ntableName foo=123 86400000000000\ntableName foo=0t 86400000000000\ntableName foo=-3600000000t 86400000000000\ntableName foo=f 86400000000000\n"; + "tableName,foo=bah 86400000000000\ntableName foo=123i 86400000000000\ntableName foo=123 86400000000000\ntableName foo=0n 86400000000000\ntableName foo=-3600000000000n 86400000000000\ntableName foo=f 86400000000000\n"; Assert.That(srv.PrintBuffer(), Is.EqualTo(expected)); } diff --git a/src/net-questdb-client-tests/TcpTests.cs b/src/net-questdb-client-tests/TcpTests.cs index 421fd32..f9cc46a 100644 --- a/src/net-questdb-client-tests/TcpTests.cs +++ b/src/net-questdb-client-tests/TcpTests.cs @@ -335,7 +335,7 @@ await sender.Table("name") await sender.SendAsync(); var expected = - "name ts=1645660800000000t 1645660800000000000\n"; + "name ts=1645660800000000000n 1645660800000000000\n"; WaitAssert(srv, expected); } diff --git a/src/net-questdb-client/Buffers/BufferV1.cs b/src/net-questdb-client/Buffers/BufferV1.cs index fa1341a..b6f586f 100644 --- a/src/net-questdb-client/Buffers/BufferV1.cs +++ b/src/net-questdb-client/Buffers/BufferV1.cs @@ -317,7 +317,7 @@ public IBuffer Column(ReadOnlySpan name, DateTime timestamp) } var epoch = timestamp.Ticks - EpochTicks; - Column(name).Put(epoch / 10).PutAscii('t'); + Column(name).Put(epoch * 100).PutAscii('n'); return this; } diff --git a/src/tcp-client-test/LineTcpSenderTests.cs b/src/tcp-client-test/LineTcpSenderTests.cs index c30d61f..7ea66e2 100644 --- a/src/tcp-client-test/LineTcpSenderTests.cs +++ b/src/tcp-client-test/LineTcpSenderTests.cs @@ -376,7 +376,7 @@ public async Task SendTimestampColumn() ls.Send(); var expected = - "name ts=1645660800000000t 1645660800000000000\n"; + "name ts=1645660800000000000n 1645660800000000000\n"; WaitAssert(srv, expected); } @@ -746,7 +746,7 @@ public async Task AtWithLongEpochNano() ls.Send(); var expected = - "name ts=1645660800000000t 1645660800000000000\n"; + "name ts=1645660800000000000n 1645660800000000000\n"; WaitAssert(srv, expected); } From 7bc544d195aa48721feb013f05ac4ecea5ce79d8 Mon Sep 17 00:00:00 2001 From: victor Date: Thu, 4 Sep 2025 09:07:21 +0800 Subject: [PATCH 2/4] add support for nanosecond precision timestamps in TCP and HTTP senders --- src/net-questdb-client-tests/HttpTests.cs | 38 ++++++++++++++++++ src/net-questdb-client-tests/TcpTests.cs | 40 +++++++++++++++++++ src/net-questdb-client/Buffers/BufferV1.cs | 21 +++++++++- src/net-questdb-client/Buffers/IBuffer.cs | 14 +++++++ .../Senders/AbstractSender.cs | 23 +++++++++++ src/net-questdb-client/Senders/ISender.cs | 19 +++++++++ src/tcp-client-test/LineTcpSenderTests.cs | 40 +++++++++++++++++++ 7 files changed, 194 insertions(+), 1 deletion(-) diff --git a/src/net-questdb-client-tests/HttpTests.cs b/src/net-questdb-client-tests/HttpTests.cs index 4f6917d..dfc4458 100644 --- a/src/net-questdb-client-tests/HttpTests.cs +++ b/src/net-questdb-client-tests/HttpTests.cs @@ -751,6 +751,44 @@ await sender.Table("name") Assert.That(srv.PrintBuffer(), Is.EqualTo(expected)); } + [Test] + public async Task SendColumnNanos() + { + using var srv = new DummyHttpServer(); + await srv.StartAsync(HttpPort); + using var sender = Sender.New($"http::addr={Host}:{HttpPort};auto_flush=off;"); + + const long timestampNanos = 1645660800123456789L; + await sender.Table("name") + .ColumnNanos("ts", timestampNanos) + .AtAsync(timestampNanos); + + await sender.SendAsync(); + + var expected = + "name ts=1645660800123456789n 1645660800123456789\n"; + Assert.That(srv.PrintBuffer(), Is.EqualTo(expected)); + } + + [Test] + public async Task SendAtNanos() + { + using var srv = new DummyHttpServer(); + await srv.StartAsync(HttpPort); + using var sender = Sender.New($"http::addr={Host}:{HttpPort};auto_flush=off;"); + + const long timestampNanos = 1645660800987654321L; + await sender.Table("name") + .Column("value", 42) + .AtNanosAsync(timestampNanos); + + await sender.SendAsync(); + + var expected = + "name value=42i 1645660800987654321\n"; + Assert.That(srv.PrintBuffer(), Is.EqualTo(expected)); + } + [Test] public async Task InvalidState() { diff --git a/src/net-questdb-client-tests/TcpTests.cs b/src/net-questdb-client-tests/TcpTests.cs index f9cc46a..a5bce4b 100644 --- a/src/net-questdb-client-tests/TcpTests.cs +++ b/src/net-questdb-client-tests/TcpTests.cs @@ -339,6 +339,46 @@ await sender.Table("name") WaitAssert(srv, expected); } + [Test] + public async Task SendColumnNanos() + { + using var srv = CreateTcpListener(_port); + srv.AcceptAsync(); + + using var sender = Sender.New($"tcp::addr={_host}:{_port};"); + + const long timestampNanos = 1645660800123456789L; + await sender.Table("name") + .ColumnNanos("ts", timestampNanos) + .AtAsync(timestampNanos); + + await sender.SendAsync(); + + var expected = + "name ts=1645660800123456789n 1645660800123456789\n"; + WaitAssert(srv, expected); + } + + [Test] + public async Task SendAtNanos() + { + using var srv = CreateTcpListener(_port); + srv.AcceptAsync(); + + using var sender = Sender.New($"tcp::addr={_host}:{_port};"); + + const long timestampNanos = 1645660800987654321L; + await sender.Table("name") + .Column("value", 42) + .AtNanosAsync(timestampNanos); + + await sender.SendAsync(); + + var expected = + "name value=42i 1645660800987654321\n"; + WaitAssert(srv, expected); + } + [Test] public Task InvalidState() { diff --git a/src/net-questdb-client/Buffers/BufferV1.cs b/src/net-questdb-client/Buffers/BufferV1.cs index b6f586f..9636ff4 100644 --- a/src/net-questdb-client/Buffers/BufferV1.cs +++ b/src/net-questdb-client/Buffers/BufferV1.cs @@ -125,6 +125,13 @@ public void At(long epochNano) FinishLine(); } + /// + public void AtNanos(long timestampNanos) + { + PutAscii(' ').Put(timestampNanos); + FinishLine(); + } + /// public void Clear() { @@ -317,7 +324,7 @@ public IBuffer Column(ReadOnlySpan name, DateTime timestamp) } var epoch = timestamp.Ticks - EpochTicks; - Column(name).Put(epoch * 100).PutAscii('n'); + Column(name).Put(epoch).PutAscii('0').PutAscii('0').PutAscii('n'); return this; } @@ -333,6 +340,18 @@ public IBuffer Column(ReadOnlySpan name, DateTimeOffset timestamp) return this; } + /// + public IBuffer ColumnNanos(ReadOnlySpan name, long timestampNanos) + { + if (WithinTransaction && !_hasTable) + { + Table(_currentTableName); + } + + Column(name).Put(timestampNanos).PutAscii('n'); + return this; + } + /// public IBuffer EncodeUtf8(ReadOnlySpan name) { diff --git a/src/net-questdb-client/Buffers/IBuffer.cs b/src/net-questdb-client/Buffers/IBuffer.cs index f363ff9..82bfdfe 100644 --- a/src/net-questdb-client/Buffers/IBuffer.cs +++ b/src/net-questdb-client/Buffers/IBuffer.cs @@ -140,6 +140,14 @@ public interface IBuffer /// Itself public IBuffer Column(ReadOnlySpan name, DateTimeOffset timestamp); + /// + /// Set value of TIMESTAMP column with exact nanosecond precision. + /// + /// Column name + /// Nanoseconds since Unix epoch + /// Itself + public IBuffer ColumnNanos(ReadOnlySpan name, long timestampNanos); + /// /// Finishes the line without specifying Designated Timestamp. QuestDB will set the timestamp at the time of writing to /// the table. @@ -164,6 +172,12 @@ public interface IBuffer /// Nanoseconds since Unix epoch public void At(long epochNano); + /// + /// Finishes the line setting timestamp with exact nanosecond precision. + /// + /// Nanoseconds since Unix epoch + public void AtNanos(long timestampNanos); + /// /// Clears the buffer. /// diff --git a/src/net-questdb-client/Senders/AbstractSender.cs b/src/net-questdb-client/Senders/AbstractSender.cs index 54b8f3c..6fcd1b0 100644 --- a/src/net-questdb-client/Senders/AbstractSender.cs +++ b/src/net-questdb-client/Senders/AbstractSender.cs @@ -122,6 +122,13 @@ public ISender Column(ReadOnlySpan name, DateTimeOffset value) return this; } + /// + public ISender ColumnNanos(ReadOnlySpan name, long timestampNanos) + { + Buffer.ColumnNanos(name, timestampNanos); + return this; + } + public ISender Column(ReadOnlySpan name, IEnumerable value, IEnumerable shape) where T : struct { Buffer.Column(name, value, shape); @@ -196,6 +203,22 @@ public void At(long value, CancellationToken ct = default) FlushIfNecessary(ct); } + /// + public ValueTask AtNanosAsync(long timestampNanos, CancellationToken ct = default) + { + GuardLastFlushNotSet(); + Buffer.AtNanos(timestampNanos); + return FlushIfNecessaryAsync(ct); + } + + /// + public void AtNanos(long timestampNanos, CancellationToken ct = default) + { + GuardLastFlushNotSet(); + Buffer.AtNanos(timestampNanos); + FlushIfNecessary(ct); + } + /// public void AtNow(CancellationToken ct = default) { diff --git a/src/net-questdb-client/Senders/ISender.cs b/src/net-questdb-client/Senders/ISender.cs index 5c49067..26101ee 100644 --- a/src/net-questdb-client/Senders/ISender.cs +++ b/src/net-questdb-client/Senders/ISender.cs @@ -149,6 +149,14 @@ public interface ISenderV1 : IDisposable /// public ISender Column(ReadOnlySpan name, DateTimeOffset value); + /// + /// Adds a timestamp column with exact nanosecond precision. + /// + /// The name of the column + /// Nanoseconds since Unix epoch + /// Itself + public ISender ColumnNanos(ReadOnlySpan name, long timestampNanos); + /// /// Adds a value for the designated timestamp column. /// @@ -180,6 +188,17 @@ public interface ISenderV1 : IDisposable /// public void At(long value, CancellationToken ct = default); + /// + /// Adds exact nanosecond precision timestamp for the designated timestamp column. + /// + /// Nanoseconds since Unix epoch + /// A cancellation token applied requests caused by auto-flushing + /// Itself + public ValueTask AtNanosAsync(long timestampNanos, CancellationToken ct = default); + + /// + public void AtNanos(long timestampNanos, CancellationToken ct = default); + /// [Obsolete("Not compatible with deduplication. Please use `At(DateTime.UtcNow)` instead.")] public void AtNow(CancellationToken ct = default); diff --git a/src/tcp-client-test/LineTcpSenderTests.cs b/src/tcp-client-test/LineTcpSenderTests.cs index 7ea66e2..2348ae2 100644 --- a/src/tcp-client-test/LineTcpSenderTests.cs +++ b/src/tcp-client-test/LineTcpSenderTests.cs @@ -750,6 +750,46 @@ public async Task AtWithLongEpochNano() WaitAssert(srv, expected); } + [Test] + public async Task SendAtNanos() + { + using var srv = CreateTcpListener(_port); + srv.AcceptAsync(); + + using var ls = await LineTcpSender.ConnectAsync("localhost", _port, tlsMode: TlsMode.Disable); + + const long timestampNanos = 1645660800987654321L; + ls.Table("name") + .Column("value", 42) + .At(timestampNanos); + + ls.Send(); + + var expected = + "name value=42i 1645660800987654321\n"; + WaitAssert(srv, expected); + } + + [Test] + public async Task SendAtNanosAsync() + { + using var srv = CreateTcpListener(_port); + srv.AcceptAsync(); + + using var ls = await LineTcpSender.ConnectAsync("localhost", _port, tlsMode: TlsMode.Disable); + + const long timestampNanos = 1645660800555666777L; + ls.Table("test_table") + .Column("measurement", 100.5) + .At(timestampNanos); + + await ls.SendAsync(); + + var expected = + "test_table measurement=100.5 1645660800555666777\n"; + WaitAssert(srv, expected); + } + private DummyIlpServer CreateTcpListener(int port, bool tls = false) { From 8f88f8fbe033dd52c196c92ad8fb93543d388d0a Mon Sep 17 00:00:00 2001 From: victor Date: Thu, 4 Sep 2025 09:36:26 +0800 Subject: [PATCH 3/4] revert deprecated api --- src/tcp-client-test/LineTcpSenderTests.cs | 44 ++--------------------- 1 file changed, 2 insertions(+), 42 deletions(-) diff --git a/src/tcp-client-test/LineTcpSenderTests.cs b/src/tcp-client-test/LineTcpSenderTests.cs index 2348ae2..c30d61f 100644 --- a/src/tcp-client-test/LineTcpSenderTests.cs +++ b/src/tcp-client-test/LineTcpSenderTests.cs @@ -376,7 +376,7 @@ public async Task SendTimestampColumn() ls.Send(); var expected = - "name ts=1645660800000000000n 1645660800000000000\n"; + "name ts=1645660800000000t 1645660800000000000\n"; WaitAssert(srv, expected); } @@ -746,47 +746,7 @@ public async Task AtWithLongEpochNano() ls.Send(); var expected = - "name ts=1645660800000000000n 1645660800000000000\n"; - WaitAssert(srv, expected); - } - - [Test] - public async Task SendAtNanos() - { - using var srv = CreateTcpListener(_port); - srv.AcceptAsync(); - - using var ls = await LineTcpSender.ConnectAsync("localhost", _port, tlsMode: TlsMode.Disable); - - const long timestampNanos = 1645660800987654321L; - ls.Table("name") - .Column("value", 42) - .At(timestampNanos); - - ls.Send(); - - var expected = - "name value=42i 1645660800987654321\n"; - WaitAssert(srv, expected); - } - - [Test] - public async Task SendAtNanosAsync() - { - using var srv = CreateTcpListener(_port); - srv.AcceptAsync(); - - using var ls = await LineTcpSender.ConnectAsync("localhost", _port, tlsMode: TlsMode.Disable); - - const long timestampNanos = 1645660800555666777L; - ls.Table("test_table") - .Column("measurement", 100.5) - .At(timestampNanos); - - await ls.SendAsync(); - - var expected = - "test_table measurement=100.5 1645660800555666777\n"; + "name ts=1645660800000000t 1645660800000000000\n"; WaitAssert(srv, expected); } From b271ac924dda8fb590bc3383289e6cb982977142 Mon Sep 17 00:00:00 2001 From: victor Date: Thu, 4 Sep 2025 22:31:39 +0800 Subject: [PATCH 4/4] use epoch*100 instead of PutAscii(' ').put('0').put('0') --- src/net-questdb-client/Buffers/BufferV1.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/net-questdb-client/Buffers/BufferV1.cs b/src/net-questdb-client/Buffers/BufferV1.cs index 9636ff4..9bd224f 100644 --- a/src/net-questdb-client/Buffers/BufferV1.cs +++ b/src/net-questdb-client/Buffers/BufferV1.cs @@ -108,7 +108,7 @@ public void AtNow() public void At(DateTime timestamp) { var epoch = timestamp.Ticks - EpochTicks; - PutAscii(' ').Put(epoch).PutAscii('0').PutAscii('0'); + PutAscii(' ').Put(epoch * 100); FinishLine(); } @@ -324,7 +324,7 @@ public IBuffer Column(ReadOnlySpan name, DateTime timestamp) } var epoch = timestamp.Ticks - EpochTicks; - Column(name).Put(epoch).PutAscii('0').PutAscii('0').PutAscii('n'); + Column(name).Put(epoch * 100).PutAscii('n'); return this; }