Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
## 1.9.0 [unreleased]

### Features

1. [#269](https://github.com/InfluxCommunity/influxdb3-csharp/pull/269): Add partial writes support and default writes to the V2 API endpoint.
- `NoSync` requires `UseV2Api=false` and the V3 API endpoint.
- `AcceptPartial` applies only when writes are sent to the V3 API endpoint and is ignored when using the V2 API endpoint.
See [Partial writes](https://docs.influxdata.com/influxdb3/core/write-data/http-api/v3-write-lp/#partial-writes) for more.

## 1.8.0 [2026-04-23]

### Features
Expand Down
42 changes: 41 additions & 1 deletion Client.Test.Integration/QueryWriteTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,46 @@ public async Task QueryWrite()
Assert.That(points.First().GetTag("type"), Is.EqualTo("used"));
}

[TestCase(true, TestName = "QueryWriteWithUseV2Api_True")]
[TestCase(false, TestName = "QueryWriteWithUseV2Api_False")]
public async Task QueryWriteWithUseV2Api(bool useV2Api)
{
using var client = new InfluxDBClient(new ClientConfig
{
Host = Host,
Token = Token,
Database = Database,
WriteOptions = new WriteOptions
{
UseV2Api = useV2Api
}
});

const string measurement = "integration_test_write_api";
var testId = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
await client.WriteRecordAsync($"{measurement},type=used value=321.0,testId={testId}");

var sql = $"SELECT value FROM {measurement} where \"testId\" = {testId}";
List<object?[]> results = new();
for (var i = 0; i < 50; i++)
{
results = await client.Query(sql).ToListAsync();
if (results.Count > 0)
{
break;
}

await Task.Delay(100);
}

Assert.Multiple((Action)(() =>
{
Assert.That(results, Has.Count.EqualTo(1), $"No rows returned for UseV2Api={useV2Api}");
Assert.That(results[0], Has.Length.EqualTo(1));
Assert.That(results[0][0], Is.EqualTo(321.0));
}));
}

[Test]
public void QueryNotAuthorized()
{
Expand Down Expand Up @@ -328,4 +368,4 @@ private static void TestQueryPoints(InfluxDBClient client, TimeSpan? timeout = n
}));
Assert.That(ex.StatusCode, Is.EqualTo(StatusCode.DeadlineExceeded));
}
}
}
50 changes: 47 additions & 3 deletions Client.Test.Integration/WriteTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Threading.Tasks;
using InfluxDB3.Client.Config;
using NUnit.Framework;
using WriteOptions = InfluxDB3.Client.Config.WriteOptions;

namespace InfluxDB3.Client.Test.Integration;

Expand All @@ -25,9 +26,8 @@ public async Task WriteWithError()
}
catch (Exception ex)
{
if (ex is InfluxDBApiException)
if (ex is InfluxDBApiException iaex)
{
var iaex = (InfluxDBApiException)ex;
Assert.Multiple((Action)(() =>
{
Assert.That(iaex.Message,
Expand All @@ -45,4 +45,48 @@ public async Task WriteWithError()
}
}
}
}

[TestCase(false, true, true, TestName = "WritePartialBatch_WithV3Api_ReturnsStructuredPartialWriteError")]
[TestCase(true, false, true, TestName = "WritePartialBatch_WithV2Api_ReturnsGenericApiError")]
[TestCase(false, true, false, TestName = "WritePartialBatch_WithV3Api_AcceptPartialFalse_ReturnsStructuredPartialWriteError")]
[TestCase(true, false, false, TestName = "WritePartialBatch_WithV2Api_AcceptPartialFalse_ReturnsGenericApiError")]
public void WritePartialBatchBehaviorByWriteApi(bool useV2Api, bool expectStructuredPartialError, bool acceptPartial)
{
using var client = new InfluxDBClient(new ClientConfig
{
Host = Host,
Token = Token,
Database = Database,
WriteOptions = new WriteOptions
{
UseV2Api = useV2Api,
AcceptPartial = acceptPartial
}
});

const string validLine = "home,room=Sunroom temp=96 1735545600";
const string invalidLine = "home,room=Sunroom temp=\"hi\" 1735549200";

var ae = Assert.CatchAsync<InfluxDBApiException>((Func<Task>)(async () =>
{
await client.WriteRecordsAsync(new[] { validLine, invalidLine });
}));

Assert.That(ae, Is.Not.Null);
Assert.That(ae!.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));

if (expectStructuredPartialError)
{
Assert.That(ae, Is.InstanceOf<InfluxDBPartialWriteException>());
var pwe = (InfluxDBPartialWriteException)ae!;
Assert.That(pwe.LineErrors, Is.Not.Empty);
Assert.That(ae.Message,
Does.Contain("partial write of line protocol occurred")
.Or.Contain("parsing failed for write_lp endpoint"));
}
else
{
Assert.That(ae, Is.Not.InstanceOf<InfluxDBPartialWriteException>());
}
}
}
8 changes: 7 additions & 1 deletion Client.Test/Config/ClientConfigTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public void CreateFromConnectionStringWithAuthScheme()
[Test]
public void CreateFromConnectionStringWithWriteOptions()
{
var cfg = new ClientConfig("http://localhost:8086?token=my-token&org=my-org&database=my-database&precision=s&gzipThreshold=64&writeNoSync=true");
var cfg = new ClientConfig("http://localhost:8086?token=my-token&org=my-org&database=my-database&precision=s&gzipThreshold=64&writeNoSync=true&writeAcceptPartial=false&writeUseV2Api=true");
Assert.That(cfg, Is.Not.Null);
cfg.Validate();
Assert.Multiple(() =>
Expand All @@ -82,6 +82,8 @@ public void CreateFromConnectionStringWithWriteOptions()
Assert.That(cfg.WriteOptions.Precision, Is.EqualTo(WritePrecision.S));
Assert.That(cfg.WriteOptions.GzipThreshold, Is.EqualTo(64));
Assert.That(cfg.WriteOptions.NoSync, Is.EqualTo(true));
Assert.That(cfg.WriteOptions.AcceptPartial, Is.EqualTo(false));
Assert.That(cfg.WriteOptions.UseV2Api, Is.EqualTo(true));
});
}

Expand Down Expand Up @@ -221,6 +223,8 @@ public void CreateFromEnvWithWriteOptions()
{"INFLUX_PRECISION", "s"},
{"INFLUX_GZIP_THRESHOLD", "64"},
{"INFLUX_WRITE_NO_SYNC", "true"},
{"INFLUX_WRITE_ACCEPT_PARTIAL", "false"},
{"INFLUX_WRITE_USE_V2_API", "true"},
};
TestUtils.SetEnv(env);
var cfg = new ClientConfig(env);
Expand All @@ -236,6 +240,8 @@ public void CreateFromEnvWithWriteOptions()
Assert.That(cfg.WriteOptions.Precision, Is.EqualTo(WritePrecision.S));
Assert.That(cfg.WriteOptions.GzipThreshold, Is.EqualTo(64));
Assert.That(cfg.WriteOptions.NoSync, Is.EqualTo(true));
Assert.That(cfg.WriteOptions.AcceptPartial, Is.EqualTo(false));
Assert.That(cfg.WriteOptions.UseV2Api, Is.EqualTo(true));
});
}

Expand Down
2 changes: 1 addition & 1 deletion Client.Test/InfluxDBApiExceptionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,4 @@ public async Task GeneratedInfluxDbException()
}
}
}
}
}
2 changes: 1 addition & 1 deletion Client.Test/InfluxDBClientHttpsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,4 +262,4 @@ private async Task QueryData()
var query = "SELECT 1";
await _client.Query(query).ToListAsync();
}
}
}
128 changes: 97 additions & 31 deletions Client.Test/InfluxDBClientWriteTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -452,31 +452,56 @@
await _client.WriteRecordAsync("mem,tag=a field=1");
}

[Test]
public async Task WriteNoSyncFalse()
private static IEnumerable<TestCaseData> WriteV3OptionCases()
{
yield return new TestCaseData(new WriteOptions { UseV2Api = false, NoSync = false }, false, false)
.SetName("WriteToV3_NoSyncFalse_OmitsNoSyncAndAcceptPartial");
yield return new TestCaseData(new WriteOptions { UseV2Api = false, AcceptPartial = false }, false, true)
.SetName("WriteToV3_AcceptPartialFalse_AddsAcceptPartialQueryParam");
yield return new TestCaseData(new WriteOptions { UseV2Api = false, NoSync = true }, true, false)
.SetName("WriteToV3_NoSyncTrue_AddsNoSyncQueryParam");
}

[TestCaseSource(nameof(WriteV3OptionCases))]
public async Task WriteToV3OptionCases(WriteOptions writeOptions, bool expectNoSync, bool expectAcceptPartialFalse)
{
_client = new InfluxDBClient(new ClientConfig
{
Host = MockServerUrl,
Token = "my-token",
Database = "my-database",
WriteOptions = new WriteOptions
{
NoSync = false
}
WriteOptions = writeOptions
});
MockServer
.Given(Request.Create().WithPath("/api/v2/write").UsingPost())
.Given(Request.Create().WithPath("/api/v3/write_lp").UsingPost())
.RespondWith(Response.Create().WithStatusCode(204));

await _client.WriteRecordAsync("mem,tag=a field=1");

var requests = MockServer.LogEntries.ToList();
Assert.That(requests[0].RequestMessage.Path, Is.EqualTo("/api/v2/write"));
using (Assert.EnterMultipleScope())
{
Assert.That(requests[0].RequestMessage.Path, Is.EqualTo("/api/v3/write_lp"));
if (expectNoSync)
{
Assert.That(requests[0].RequestMessage.Query?["no_sync"].First(), Is.EqualTo("true"));
}
else
{
Assert.That(requests[0].RequestMessage.Query, Does.Not.ContainKey("no_sync"));
}
if (expectAcceptPartialFalse)
{
Assert.That(requests[0].RequestMessage.Query?["accept_partial"].First(), Is.EqualTo("false"));
}
else
{
Assert.That(requests[0].RequestMessage.Query, Does.Not.ContainKey("accept_partial"));
}
}
}

[Test]
public async Task WriteNoSyncTrueSupported()
public void WriteNoSyncTrueWithUseV2ApiValidationError()
{
_client = new InfluxDBClient(new ClientConfig
{
Expand All @@ -485,21 +510,26 @@
Database = "my-database",
WriteOptions = new WriteOptions
{
NoSync = true
NoSync = true,
UseV2Api = true
}
});
MockServer
.Given(Request.Create().WithPath("/api/v3/write_lp").UsingPost())
.RespondWith(Response.Create().WithStatusCode(204));

await _client.WriteRecordAsync("mem,tag=a field=1");
var ae = Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
await _client.WriteRecordAsync("mem,tag=a field=1");
});

var requests = MockServer.LogEntries.ToList();
Assert.That(requests[0].RequestMessage.Query?["no_sync"].First(), Is.EqualTo("true"));
Assert.Multiple(() =>
{
Assert.That(ae, Is.Not.Null);
Assert.That(ae.Message, Is.EqualTo("Invalid write options: NoSync requires UseV2Api=false"));
});
}

[Test]
public void WriteNoSyncTrueNotSupported()
[TestCase(true)]
[TestCase(false)]
public async Task WriteUseV2ApiRoutesToV2AndIgnoresAcceptPartial(bool acceptPartial)
{
_client = new InfluxDBClient(new ClientConfig
{
Expand All @@ -508,27 +538,61 @@
Database = "my-database",
WriteOptions = new WriteOptions
{
NoSync = true
UseV2Api = true,
AcceptPartial = acceptPartial
}
});
MockServer
.Given(Request.Create().WithPath("/api/v3/write_lp").UsingPost())
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.MethodNotAllowed));
.Given(Request.Create().WithPath("/api/v2/write").UsingPost())
.RespondWith(Response.Create().WithStatusCode(204));

await _client.WriteRecordAsync("mem,tag=a field=1");

var ae = Assert.ThrowsAsync<InfluxDBApiException>(async () =>
var requests = MockServer.LogEntries.ToList();
using (Assert.EnterMultipleScope())
{
await _client.WriteRecordAsync("mem,tag=a field=1");
});
Assert.That(requests[0].RequestMessage.Path, Is.EqualTo("/api/v2/write"));
Assert.That(requests[0].RequestMessage.Query?["bucket"].First(), Is.EqualTo("my-database"));
Assert.That(requests[0].RequestMessage.Query?["precision"].First(), Is.EqualTo("ns"));
Assert.That(requests[0].RequestMessage.Query, Does.Not.ContainKey("accept_partial"));
Assert.That(requests[0].RequestMessage.Query, Does.Not.ContainKey("no_sync"));
}
}

Assert.Multiple(() =>
[Test]
public async Task WriteDefaultV2ToV3OnlyBackendReturnsGuidance()
{
_client = new InfluxDBClient(MockServerUrl, token: "my-token", database: "my-database");
MockServer
.Given(Request.Create().WithPath("/api/v2/write").UsingPost())
.RespondWith(Response.Create().WithStatusCode(405));

var ae = Assert.ThrowsAsync<InfluxDBApiException>(async () => { await _client.WriteRecordAsync("mem,tag=a field=1"); });
Assert.That(ae, Is.Not.Null);
Assert.That(ae!.Message, Is.EqualTo(
"Server doesn't support the V2 API endpoint (/api/v2/write) " +
"(set UseV2Api=false; write options: {UseV2Api:true,NoSync:false,AcceptPartial:true})"));
}

[Test]
public async Task WriteV3ToV2OnlyBackendReturnsGuidance()
{
_client = new InfluxDBClient(new ClientConfig
{
Assert.That(ae, Is.Not.Null);
Assert.That(ae.HttpResponseMessage, Is.Not.Null);
Assert.That(ae.Message,
Does.Contain(
"Server doesn't support write with NoSync=true (supported by InfluxDB 3 Core/Enterprise servers only)."));
Host = MockServerUrl,
Token = "my-token",
Database = "my-database",
WriteOptions = new WriteOptions { UseV2Api = false }
});
MockServer
.Given(Request.Create().WithPath("/api/v3/write_lp").UsingPost())
.RespondWith(Response.Create().WithStatusCode(405));

var ae = Assert.ThrowsAsync<InfluxDBApiException>(async () => { await _client.WriteRecordAsync("mem,tag=a field=1"); });
Assert.That(ae, Is.Not.Null);
Assert.That(ae!.Message, Is.EqualTo(
"Server doesn't support the V3 API endpoint (/api/v3/write_lp) " +
"(set UseV2Api=true; write options: {UseV2Api:false,NoSync:false,AcceptPartial:true})"));
}

[Test]
Expand All @@ -543,7 +607,7 @@
Host = MockServerUrl,
Token = "my-token",
Database = "my-database",
Timeout = TimeSpan.FromTicks(1)

Check warning on line 610 in Client.Test/InfluxDBClientWriteTest.cs

View workflow job for this annotation

GitHub Actions / CodeQL-Build

'ClientConfig.Timeout' is obsolete: 'Please use more informative properties like WriteTimeout or QueryTimeout'
});
TestWriteRecordAsync(_client);
TestWriteRecordsAsync(_client);
Expand All @@ -564,7 +628,7 @@
Token = "my-token",
Database = "my-database",
QueryTimeout = TimeSpan.FromSeconds(11),
Timeout = TimeSpan.FromSeconds(11),

Check warning on line 631 in Client.Test/InfluxDBClientWriteTest.cs

View workflow job for this annotation

GitHub Actions / CodeQL-Build

'ClientConfig.Timeout' is obsolete: 'Please use more informative properties like WriteTimeout or QueryTimeout'
WriteTimeout = TimeSpan.FromTicks(1) // WriteTimeout has a higher priority than Timeout
});
TestWriteRecordAsync(_client);
Expand All @@ -587,7 +651,7 @@
Token = "my-token",
Database = "my-database",
QueryTimeout = TimeSpan.FromSeconds(11),
Timeout = TimeSpan.FromSeconds(11),

Check warning on line 654 in Client.Test/InfluxDBClientWriteTest.cs

View workflow job for this annotation

GitHub Actions / CodeQL-Build

'ClientConfig.Timeout' is obsolete: 'Please use more informative properties like WriteTimeout or QueryTimeout'
WriteTimeout = TimeSpan.FromSeconds(11)
});
var cancellationToken = new CancellationTokenSource(TimeSpan.FromTicks(1)).Token;
Expand Down Expand Up @@ -619,6 +683,7 @@
parameters.Add("org", org);
parameters.Add("precision", precision);
parameters.Add("writeNoSync", writeNoSync.ToString().ToLower());
parameters.Add("writeUseV2Api", "false");
parameters.Add("gzipThreshold", gzipThreshold.ToString()); ;
var uriBuilder = new UriBuilder(MockServerUrl);
uriBuilder.Query = parameters.ToString()!;
Expand Down Expand Up @@ -673,6 +738,7 @@
{ ClientConfig.EnvInfluxPrecision, precision },
{ ClientConfig.EnvInfluxAuthScheme, authSchema },
{ ClientConfig.EnvInfluxWriteNoSync, writeNoSync.ToString().ToLower() },
{ ClientConfig.EnvInfluxWriteUseV2Api, "false" },
{ ClientConfig.EnvInfluxGzipThreshold, gzipThreshold.ToString() }
};

Expand Down
Loading
Loading