From 7a1418742b5ecb7214b5e651d5dd5ba3cd799210 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Mon, 2 Mar 2026 09:12:50 +0100 Subject: [PATCH 01/14] feat: add support for custom tag order --- README.md | 13 ++++ .../java/com/influxdb/v3/client/Point.java | 63 ++++++++++++++++--- .../client/internal/InfluxDBClientImpl.java | 6 +- .../v3/client/write/WriteOptions.java | 62 ++++++++++++++++-- .../v3/client/InfluxDBClientWriteTest.java | 19 ++++++ .../com/influxdb/v3/client/PointTest.java | 42 +++++++++---- .../v3/client/write/WriteOptionsTest.java | 12 ++++ 7 files changed, 190 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index aa7ce6ed..7c7c6c1e 100644 --- a/README.md +++ b/README.md @@ -69,11 +69,13 @@ To start with the client, import the `com.influxdb.v3.client` package and create package com.influxdb.v3; import java.time.Instant; +import java.util.List; import java.util.stream.Stream; import com.influxdb.v3.client.InfluxDBClient; import com.influxdb.v3.client.query.QueryOptions; import com.influxdb.v3.client.Point; +import com.influxdb.v3.client.write.WriteOptions; public class IOxExample { public static void main(String[] args) throws Exception { @@ -100,6 +102,17 @@ Point point = Point.measurement("temperature") .setTimestamp(Instant.now().minusSeconds(-10)); client.writePoint(point); +WriteOptions orderedTagWrite = new WriteOptions.Builder() + .tagOrder(List.of("region", "host")) + .build(); +client.writePoint( + Point.measurement("temperature") + .setTag("host", "server-1") + .setTag("region", "eu-west") + .setField("value", 60.25), + orderedTagWrite +); + // // Write by LineProtocol // diff --git a/src/main/java/com/influxdb/v3/client/Point.java b/src/main/java/com/influxdb/v3/client/Point.java index e0c0e26c..b308c323 100644 --- a/src/main/java/com/influxdb/v3/client/Point.java +++ b/src/main/java/com/influxdb/v3/client/Point.java @@ -24,9 +24,14 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; import java.time.Instant; import java.util.Locale; import java.util.Map; +import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.TimeUnit; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -530,7 +535,7 @@ public Point copy() { */ @Nonnull public String toLineProtocol() { - return toLineProtocol(null); + return toLineProtocol(null, null, null); } /** @@ -541,11 +546,26 @@ public String toLineProtocol() { */ @Nonnull public String toLineProtocol(@Nullable final WritePrecision precision) { + return toLineProtocol(precision, null, null); + } + + /** + * Transform to Line Protocol. + * + * @param precision required precision + * @param defaultTags default tags to include in point serialization + * @param tagOrder preferred order of tags in point serialization + * @return Line Protocol + */ + @Nonnull + public String toLineProtocol(@Nullable final WritePrecision precision, + @Nullable final Map defaultTags, + @Nullable final List tagOrder) { StringBuilder sb = new StringBuilder(); escapeKey(sb, getMeasurement(), false); - appendTags(sb); + appendTags(sb, defaultTags, tagOrder); boolean appendedFields = appendFields(sb); if (!appendedFields) { return ""; @@ -564,16 +584,45 @@ private Point putField(@Nonnull final String field, @Nullable final Object value return this; } - private void appendTags(@Nonnull final StringBuilder sb) { + private void appendTags(@Nonnull final StringBuilder sb, + @Nullable final Map defaultTags, + @Nullable final List tagOrder) { + Set remaining = new TreeSet<>(); + for (String pointTag : values.getTagNames()) { + if (pointTag != null && !pointTag.isEmpty()) { + remaining.add(pointTag); + } + } + if (defaultTags != null) { + for (String defaultTag : defaultTags.keySet()) { + if (defaultTag != null && !defaultTag.isEmpty()) { + remaining.add(defaultTag); + } + } + } - for (String name : values.getTagNames()) { + List orderedTagNames = new ArrayList<>(); + if (tagOrder != null && !tagOrder.isEmpty()) { + Set seen = new HashSet<>(); + for (String preferredTag : tagOrder) { + if (preferredTag == null || preferredTag.isEmpty() || !seen.add(preferredTag)) { + continue; + } + if (remaining.remove(preferredTag)) { + orderedTagNames.add(preferredTag); + } + } + } + orderedTagNames.addAll(remaining); + for (String name : orderedTagNames) { String value = values.getTag(name); - - if (name.isEmpty() || value == null || value.isEmpty()) { + if (defaultTags != null && defaultTags.containsKey(name)) { + value = defaultTags.get(name); + } + if (value == null || value.isEmpty()) { continue; } - sb.append(','); escapeKey(sb, name); sb.append('='); diff --git a/src/main/java/com/influxdb/v3/client/internal/InfluxDBClientImpl.java b/src/main/java/com/influxdb/v3/client/internal/InfluxDBClientImpl.java index 50db570a..2fb71864 100644 --- a/src/main/java/com/influxdb/v3/client/internal/InfluxDBClientImpl.java +++ b/src/main/java/com/influxdb/v3/client/internal/InfluxDBClientImpl.java @@ -330,15 +330,13 @@ private void writeData(@Nonnull final List data, @Nonnull final WriteOpti } Map defaultTags = options.defaultTagsSafe(config); + List tagOrder = options.tagOrderSafe(); String lineProtocol = data.stream().map(item -> { if (item == null) { return null; } else if (item instanceof Point) { - for (String key : defaultTags.keySet()) { - ((Point) item).setTag(key, defaultTags.get(key)); - } - return ((Point) item).toLineProtocol(); + return ((Point) item).toLineProtocol(null, defaultTags, tagOrder); } else { return item.toString(); } diff --git a/src/main/java/com/influxdb/v3/client/write/WriteOptions.java b/src/main/java/com/influxdb/v3/client/write/WriteOptions.java index ca491f1d..f6c22a4a 100644 --- a/src/main/java/com/influxdb/v3/client/write/WriteOptions.java +++ b/src/main/java/com/influxdb/v3/client/write/WriteOptions.java @@ -22,6 +22,7 @@ package com.influxdb.v3.client.write; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import javax.annotation.Nonnull; @@ -40,6 +41,7 @@ *
  • organization - specifies the organization to be used for InfluxDB operations
  • *
  • precision - specifies the precision to use for the timestamp of points
  • *
  • defaultTags - specifies tags to be added by default to all write operations using points.
  • + *
  • tagOrder - specifies preferred tag order for point serialization.
  • *
  • headers - specifies the headers to be added to write request
  • * *

    @@ -78,13 +80,14 @@ public final class WriteOptions { @Deprecated(forRemoval = true) public static final WriteOptions DEFAULTS = new WriteOptions( - null, DEFAULT_WRITE_PRECISION, DEFAULT_GZIP_THRESHOLD, DEFAULT_NO_SYNC, null, null); + null, DEFAULT_WRITE_PRECISION, DEFAULT_GZIP_THRESHOLD, DEFAULT_NO_SYNC, null, null, null); private final String database; private final WritePrecision precision; private final Integer gzipThreshold; private final Boolean noSync; private final Map defaultTags; + private final List tagOrder; private final Map headers; /** @@ -94,7 +97,8 @@ public final class WriteOptions { * compression threshold, and no specified database. */ public static WriteOptions defaultWriteOptions() { - return new WriteOptions(null, DEFAULT_WRITE_PRECISION, DEFAULT_GZIP_THRESHOLD, DEFAULT_NO_SYNC, null, null); + return new WriteOptions(null, DEFAULT_WRITE_PRECISION, DEFAULT_GZIP_THRESHOLD, DEFAULT_NO_SYNC, + null, null, null); } /** @@ -204,11 +208,39 @@ public WriteOptions(@Nullable final String database, @Nullable final Boolean noSync, @Nullable final Map defaultTags, @Nullable final Map headers) { + this(database, precision, gzipThreshold, noSync, defaultTags, headers, null); + } + + /** + * Construct WriteAPI options. + * + * @param database The database to be used for InfluxDB operations. + * If it is not specified then use {@link ClientConfig#getDatabase()}. + * @param precision The precision to use for the timestamp of points. + * If it is not specified then use {@link ClientConfig#getWritePrecision()}. + * @param gzipThreshold The threshold for compressing request body. + * If it is not specified then use {@link WriteOptions#DEFAULT_GZIP_THRESHOLD}. + * @param noSync Skip waiting for WAL persistence on write. + * If it is not specified then use {@link WriteOptions#DEFAULT_NO_SYNC}. + * @param defaultTags Default tags to be added when writing points. + * @param headers The headers to be added to write request. + * The headers specified here are preferred over the headers + * specified in the client configuration. + * @param tagOrder Preferred order of tags in line protocol serialization. + */ + public WriteOptions(@Nullable final String database, + @Nullable final WritePrecision precision, + @Nullable final Integer gzipThreshold, + @Nullable final Boolean noSync, + @Nullable final Map defaultTags, + @Nullable final Map headers, + @Nullable final List tagOrder) { this.database = database; this.precision = precision; this.gzipThreshold = gzipThreshold; this.noSync = noSync; this.defaultTags = defaultTags == null ? Map.of() : defaultTags; + this.tagOrder = tagOrder == null ? List.of() : List.copyOf(tagOrder); this.headers = headers == null ? Map.of() : headers; } @@ -277,6 +309,14 @@ public Map headersSafe() { return headers; } + /** + * @return preferred order of tags in line protocol serialization. + */ + @Nonnull + public List tagOrderSafe() { + return tagOrder; + } + @Override public boolean equals(final Object o) { if (this == o) { @@ -291,12 +331,13 @@ public boolean equals(final Object o) { && Objects.equals(gzipThreshold, that.gzipThreshold) && Objects.equals(noSync, that.noSync) && defaultTags.equals(that.defaultTags) + && tagOrder.equals(that.tagOrder) && headers.equals(that.headers); } @Override public int hashCode() { - return Objects.hash(database, precision, gzipThreshold, noSync, defaultTags, headers); + return Objects.hash(database, precision, gzipThreshold, noSync, defaultTags, tagOrder, headers); } private boolean isNotDefined(final String option) { @@ -314,6 +355,7 @@ public static final class Builder { private Integer gzipThreshold; private Boolean noSync; private Map defaultTags = new HashMap<>(); + private List tagOrder = List.of(); private Map headers = new HashMap<>(); /** @@ -380,6 +422,18 @@ public Builder defaultTags(@Nonnull final Map defaultTags) { return this; } + /** + * Sets preferred tag order for line protocol serialization. + * + * @param tagOrder tag order preference + * @return this + */ + @Nonnull + public Builder tagOrder(@Nonnull final List tagOrder) { + this.tagOrder = List.copyOf(tagOrder); + return this; + } + /** * Sets the headers. * @@ -406,6 +460,6 @@ public WriteOptions build() { private WriteOptions(@Nonnull final Builder builder) { this(builder.database, builder.precision, builder.gzipThreshold, builder.noSync, builder.defaultTags, - builder.headers); + builder.headers, builder.tagOrder); } } diff --git a/src/test/java/com/influxdb/v3/client/InfluxDBClientWriteTest.java b/src/test/java/com/influxdb/v3/client/InfluxDBClientWriteTest.java index cd4242d7..a7d31240 100644 --- a/src/test/java/com/influxdb/v3/client/InfluxDBClientWriteTest.java +++ b/src/test/java/com/influxdb/v3/client/InfluxDBClientWriteTest.java @@ -512,6 +512,25 @@ void defaultTags() throws InterruptedException { // assertThat(request.getBody().readUtf8()).isEqualTo("mem,model=M5,tag=one,unit=U2 value=1.0"); assertThat(request.getBody().utf8()).isEqualTo("mem,model=M5,tag=one,unit=U2 value=1.0"); + mockServer.enqueue(createResponse(200)); + + Point orderedPoint = Point.measurement("mem") + .setTag("host", "h1") + .setTag("unit", "point-unit") + .setField("value", 1.0); + + WriteOptions optionsWithTagOrder = new WriteOptions.Builder() + .defaultTags(defaultTags) + .tagOrder(List.of("unit", "host")) + .build(); + + client.writePoint(orderedPoint, optionsWithTagOrder); + + assertThat(mockServer.getRequestCount()).isEqualTo(2); + RecordedRequest orderedRequest = mockServer.takeRequest(); + assertThat(orderedRequest).isNotNull(); + assertThat(orderedRequest.getBody().utf8()).isEqualTo("mem,unit=U2,host=h1,model=M5 value=1.0"); + } @Test diff --git a/src/test/java/com/influxdb/v3/client/PointTest.java b/src/test/java/com/influxdb/v3/client/PointTest.java index 01d79440..b97531fe 100644 --- a/src/test/java/com/influxdb/v3/client/PointTest.java +++ b/src/test/java/com/influxdb/v3/client/PointTest.java @@ -21,10 +21,11 @@ */ package com.influxdb.v3.client; -import java.math.BigInteger; -import java.time.Instant; -import java.util.HashMap; -import java.util.Map; +import java.math.BigInteger; +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; @@ -188,14 +189,31 @@ void getFieldNames() { } @Test - void toLineProtocol() { - Point point = Point.measurement("measurement") - .setTag("tag1", "value1") - .setField("field1", 42); - - String lineProtocol = point.toLineProtocol(WritePrecision.NS); - Assertions.assertThat("measurement,tag1=value1 field1=42i").isEqualTo(lineProtocol); - } + void toLineProtocol() { + Point point = Point.measurement("measurement") + .setTag("tag1", "value1") + .setField("field1", 42); + + String lineProtocol = point.toLineProtocol(WritePrecision.NS); + Assertions.assertThat("measurement,tag1=value1 field1=42i").isEqualTo(lineProtocol); + } + + @Test + void toLineProtocolWithTagOrder() { + Point point = Point.measurement("measurement") + .setTag("host", "h1") + .setTag("region", "point-region") + .setField("field1", 42); + + String lineProtocol = point.toLineProtocol( + WritePrecision.NS, + Map.of("region", "default-region", "rack", "r1"), + List.of("region", "host") + ); + + Assertions.assertThat("measurement,region=default-region,host=h1,rack=r1 field1=42i") + .isEqualTo(lineProtocol); + } @Test void copy() { diff --git a/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java b/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java index b63d98b4..ad34cf8d 100644 --- a/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java +++ b/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java @@ -23,6 +23,7 @@ import java.util.HashMap; +import java.util.List; import java.util.Map; import org.assertj.core.api.Assertions; @@ -92,6 +93,15 @@ void optionsWithHeaders() { Assertions.assertThat(options).isEqualTo(optionsViaBuilder); } + @Test + void optionsWithTagOrder() { + List tagOrder = List.of("region", "host"); + + WriteOptions options = new WriteOptions.Builder().tagOrder(tagOrder).build(); + + Assertions.assertThat(options.tagOrderSafe()).containsExactly("region", "host"); + } + @Test void optionsEmpty() { ClientConfig config = configBuilder @@ -256,5 +266,7 @@ void optionsHashCode() { .isNotEqualTo(builder.database("my-database").build().hashCode()); Assertions.assertThat(baseOptions.hashCode()) .isNotEqualTo(builder.defaultTags(defaultTags).build().hashCode()); + Assertions.assertThat(baseOptions.hashCode()) + .isNotEqualTo(builder.tagOrder(List.of("region", "host")).build().hashCode()); } } From dcae4e98100d42a947a67d78681f73f580c71338 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Mon, 2 Mar 2026 09:15:47 +0100 Subject: [PATCH 02/14] docs: update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50769a30..df16256c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ### Features 1. [#360](https://github.com/InfluxCommunity/influxdb3-java/pull/360): Support passing interceptors to the Flight client. +1. [#363](https://github.com/InfluxCommunity/influxdb3-java/pull/363): Support custom tag order via `tagOrder` write option. + See [Sort tags by priority](https://docs.influxdata.com/influxdb3/enterprise/write-data/best-practices/schema-design/#sort-tags-by-query-priority) for more. ## 1.8.0 [2026-02-19] From 08497c4b3691543eea2f9692e62be6d1cb225133 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Mon, 2 Mar 2026 09:17:27 +0100 Subject: [PATCH 03/14] fix: import order --- src/main/java/com/influxdb/v3/client/Point.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/influxdb/v3/client/Point.java b/src/main/java/com/influxdb/v3/client/Point.java index b308c323..85febb44 100644 --- a/src/main/java/com/influxdb/v3/client/Point.java +++ b/src/main/java/com/influxdb/v3/client/Point.java @@ -24,10 +24,10 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.text.NumberFormat; +import java.time.Instant; import java.util.ArrayList; import java.util.HashSet; import java.util.List; -import java.time.Instant; import java.util.Locale; import java.util.Map; import java.util.Set; From 688845dc2b10044f3f6d9822ff80bd86aabc1594 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Mon, 2 Mar 2026 09:51:59 +0100 Subject: [PATCH 04/14] test: more coverage --- .../com/influxdb/v3/client/PointTest.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/test/java/com/influxdb/v3/client/PointTest.java b/src/test/java/com/influxdb/v3/client/PointTest.java index b97531fe..887fbc2b 100644 --- a/src/test/java/com/influxdb/v3/client/PointTest.java +++ b/src/test/java/com/influxdb/v3/client/PointTest.java @@ -23,6 +23,7 @@ import java.math.BigInteger; import java.time.Instant; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -213,6 +214,27 @@ void toLineProtocolWithTagOrder() { Assertions.assertThat("measurement,region=default-region,host=h1,rack=r1 field1=42i") .isEqualTo(lineProtocol); + + Point pointWithIgnoredTags = Point.measurement("measurement") + .setTag("", "ignored") + .setTag("host", "h1") + .setTag("region", "point-region") + .setField("field1", 42); + + Map defaultTags = new HashMap<>(); + defaultTags.put("", "ignored"); + defaultTags.put(null, "ignored"); + defaultTags.put("rack", "r1"); + defaultTags.put("zone", "z1"); + + String lineProtocolWithIgnoredTagOrderEntries = pointWithIgnoredTags.toLineProtocol( + WritePrecision.NS, + defaultTags, + Arrays.asList("region", "", null, "region", "missing", "host") + ); + + Assertions.assertThat("measurement,region=point-region,host=h1,rack=r1,zone=z1 field1=42i") + .isEqualTo(lineProtocolWithIgnoredTagOrderEntries); } @Test From 4e0d334fd88927061c1ebc372a5603443d0e5362 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Mon, 2 Mar 2026 10:46:28 +0100 Subject: [PATCH 05/14] test: more coverage --- src/main/java/com/influxdb/v3/client/Point.java | 2 +- .../java/com/influxdb/v3/client/write/WriteOptionsTest.java | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/influxdb/v3/client/Point.java b/src/main/java/com/influxdb/v3/client/Point.java index 85febb44..d8e5bcb4 100644 --- a/src/main/java/com/influxdb/v3/client/Point.java +++ b/src/main/java/com/influxdb/v3/client/Point.java @@ -589,7 +589,7 @@ private void appendTags(@Nonnull final StringBuilder sb, @Nullable final List tagOrder) { Set remaining = new TreeSet<>(); for (String pointTag : values.getTagNames()) { - if (pointTag != null && !pointTag.isEmpty()) { + if (!pointTag.isEmpty()) { remaining.add(pointTag); } } diff --git a/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java b/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java index ad34cf8d..30c6db3a 100644 --- a/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java +++ b/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java @@ -111,11 +111,15 @@ void optionsEmpty() { .gzipThreshold(512) .build(); - WriteOptions options = new WriteOptions.Builder().build(); + WriteOptions options = WriteOptions.defaultWriteOptions(); Assertions.assertThat(options.databaseSafe(config)).isEqualTo("my-database"); Assertions.assertThat(options.precisionSafe(config)).isEqualTo(WritePrecision.S); Assertions.assertThat(options.gzipThresholdSafe(config)).isEqualTo(512); + Assertions.assertThat(options.tagOrderSafe()).isEmpty(); + + WriteOptions builderOptions = new WriteOptions.Builder().build(); + Assertions.assertThat(builderOptions).isEqualTo(options); } @Test From eda6fc6fb726a6a70f86713bf19db44cec37dbb9 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Mon, 2 Mar 2026 10:51:43 +0100 Subject: [PATCH 06/14] test: fix eq test --- .../com/influxdb/v3/client/write/WriteOptionsTest.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java b/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java index 30c6db3a..3b0731bd 100644 --- a/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java +++ b/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java @@ -114,12 +114,14 @@ void optionsEmpty() { WriteOptions options = WriteOptions.defaultWriteOptions(); Assertions.assertThat(options.databaseSafe(config)).isEqualTo("my-database"); - Assertions.assertThat(options.precisionSafe(config)).isEqualTo(WritePrecision.S); - Assertions.assertThat(options.gzipThresholdSafe(config)).isEqualTo(512); + Assertions.assertThat(options.precisionSafe(config)).isEqualTo(WriteOptions.DEFAULT_WRITE_PRECISION); + Assertions.assertThat(options.gzipThresholdSafe(config)).isEqualTo(WriteOptions.DEFAULT_GZIP_THRESHOLD); Assertions.assertThat(options.tagOrderSafe()).isEmpty(); WriteOptions builderOptions = new WriteOptions.Builder().build(); - Assertions.assertThat(builderOptions).isEqualTo(options); + Assertions.assertThat(builderOptions.databaseSafe(config)).isEqualTo("my-database"); + Assertions.assertThat(builderOptions.precisionSafe(config)).isEqualTo(WritePrecision.S); + Assertions.assertThat(builderOptions.gzipThresholdSafe(config)).isEqualTo(512); } @Test From 5e3b60ef0000198547a72640f4301c0adea6338d Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Mon, 2 Mar 2026 11:28:14 +0100 Subject: [PATCH 07/14] test: more coverage --- .../v3/client/write/WriteOptionsTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java b/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java index 3b0731bd..a88131fd 100644 --- a/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java +++ b/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java @@ -61,6 +61,27 @@ void optionsEqualAll() { .database("my-database").precision(WritePrecision.S).gzipThreshold(512).noSync(true).build(); Assertions.assertThat(options).isEqualTo(optionsViaBuilder); + + // Exercise each equals() comparison field (lines 331-335) with a mismatch. + WriteOptions gzipMismatch = new WriteOptions.Builder() + .database("my-database").precision(WritePrecision.S).gzipThreshold(1024).noSync(true).build(); + WriteOptions noSyncMismatch = new WriteOptions.Builder() + .database("my-database").precision(WritePrecision.S).gzipThreshold(512).noSync(false).build(); + WriteOptions defaultTagsMismatch = new WriteOptions.Builder() + .database("my-database").precision(WritePrecision.S).gzipThreshold(512).noSync(true) + .defaultTags(Map.of("region", "west")).build(); + WriteOptions tagOrderMismatch = new WriteOptions.Builder() + .database("my-database").precision(WritePrecision.S).gzipThreshold(512).noSync(true) + .tagOrder(List.of("host", "region")).build(); + WriteOptions headersMismatch = new WriteOptions.Builder() + .database("my-database").precision(WritePrecision.S).gzipThreshold(512).noSync(true) + .headers(Map.of("X-Trace-Id", "123")).build(); + + Assertions.assertThat(options).isNotEqualTo(gzipMismatch); + Assertions.assertThat(options).isNotEqualTo(noSyncMismatch); + Assertions.assertThat(options).isNotEqualTo(defaultTagsMismatch); + Assertions.assertThat(options).isNotEqualTo(tagOrderMismatch); + Assertions.assertThat(options).isNotEqualTo(headersMismatch); } @Test From aba19727214846c00c7da957d7a791b3964e6d84 Mon Sep 17 00:00:00 2001 From: alespour <42931850+alespour@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:38:46 +0100 Subject: [PATCH 08/14] test: comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../java/com/influxdb/v3/client/write/WriteOptionsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java b/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java index a88131fd..3de5e5e5 100644 --- a/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java +++ b/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java @@ -62,7 +62,7 @@ void optionsEqualAll() { Assertions.assertThat(options).isEqualTo(optionsViaBuilder); - // Exercise each equals() comparison field (lines 331-335) with a mismatch. + // Exercise each field used by WriteOptions.equals() with a mismatch. WriteOptions gzipMismatch = new WriteOptions.Builder() .database("my-database").precision(WritePrecision.S).gzipThreshold(1024).noSync(true).build(); WriteOptions noSyncMismatch = new WriteOptions.Builder() From 78d4ad132f27903ac8b74f3487f3976652595d26 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Mon, 2 Mar 2026 11:45:02 +0100 Subject: [PATCH 09/14] fix: refactoring --- .../java/com/influxdb/v3/client/Point.java | 26 ++++++++++++++----- .../v3/client/write/WriteOptions.java | 19 +++++++++++--- .../v3/client/write/WriteOptionsTest.java | 12 ++++++++- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/influxdb/v3/client/Point.java b/src/main/java/com/influxdb/v3/client/Point.java index d8e5bcb4..bbeb7895 100644 --- a/src/main/java/com/influxdb/v3/client/Point.java +++ b/src/main/java/com/influxdb/v3/client/Point.java @@ -587,6 +587,14 @@ private Point putField(@Nonnull final String field, @Nullable final Object value private void appendTags(@Nonnull final StringBuilder sb, @Nullable final Map defaultTags, @Nullable final List tagOrder) { + if ((defaultTags == null || defaultTags.isEmpty()) && (tagOrder == null || tagOrder.isEmpty())) { + for (String name : values.getTagNames()) { + appendTag(sb, name, values.getTag(name)); + } + sb.append(' '); + return; + } + Set remaining = new TreeSet<>(); for (String pointTag : values.getTagNames()) { if (!pointTag.isEmpty()) { @@ -620,17 +628,21 @@ private void appendTags(@Nonnull final StringBuilder sb, if (defaultTags != null && defaultTags.containsKey(name)) { value = defaultTags.get(name); } - if (value == null || value.isEmpty()) { - continue; - } - sb.append(','); - escapeKey(sb, name); - sb.append('='); - escapeKey(sb, value); + appendTag(sb, name, value); } sb.append(' '); } + private void appendTag(@Nonnull final StringBuilder sb, @Nullable final String name, @Nullable final String value) { + if (name == null || name.isEmpty() || value == null || value.isEmpty()) { + return; + } + sb.append(','); + escapeKey(sb, name); + sb.append('='); + escapeKey(sb, value); + } + private boolean appendFields(@Nonnull final StringBuilder sb) { boolean appended = false; diff --git a/src/main/java/com/influxdb/v3/client/write/WriteOptions.java b/src/main/java/com/influxdb/v3/client/write/WriteOptions.java index f6c22a4a..975cde80 100644 --- a/src/main/java/com/influxdb/v3/client/write/WriteOptions.java +++ b/src/main/java/com/influxdb/v3/client/write/WriteOptions.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; @@ -227,6 +228,7 @@ public WriteOptions(@Nullable final String database, * The headers specified here are preferred over the headers * specified in the client configuration. * @param tagOrder Preferred order of tags in line protocol serialization. + * Null or empty tag names are ignored. */ public WriteOptions(@Nullable final String database, @Nullable final WritePrecision precision, @@ -240,7 +242,7 @@ public WriteOptions(@Nullable final String database, this.gzipThreshold = gzipThreshold; this.noSync = noSync; this.defaultTags = defaultTags == null ? Map.of() : defaultTags; - this.tagOrder = tagOrder == null ? List.of() : List.copyOf(tagOrder); + this.tagOrder = sanitizeTagOrder(tagOrder); this.headers = headers == null ? Map.of() : headers; } @@ -344,6 +346,17 @@ private boolean isNotDefined(final String option) { return option == null || option.isEmpty(); } + @Nonnull + private static List sanitizeTagOrder(@Nullable final List tagOrder) { + if (tagOrder == null || tagOrder.isEmpty()) { + return List.of(); + } + return tagOrder.stream() + .filter(Objects::nonNull) + .filter(tag -> !tag.isEmpty()) + .collect(Collectors.toUnmodifiableList()); + } + /** * A builder for {@code WriteOptions}. *

    @@ -425,12 +438,12 @@ public Builder defaultTags(@Nonnull final Map defaultTags) { /** * Sets preferred tag order for line protocol serialization. * - * @param tagOrder tag order preference + * @param tagOrder tag order preference. Null or empty tag names are ignored. * @return this */ @Nonnull public Builder tagOrder(@Nonnull final List tagOrder) { - this.tagOrder = List.copyOf(tagOrder); + this.tagOrder = sanitizeTagOrder(tagOrder); return this; } diff --git a/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java b/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java index 3de5e5e5..a1b7974c 100644 --- a/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java +++ b/src/test/java/com/influxdb/v3/client/write/WriteOptionsTest.java @@ -22,6 +22,7 @@ package com.influxdb.v3.client.write; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -62,7 +63,6 @@ void optionsEqualAll() { Assertions.assertThat(options).isEqualTo(optionsViaBuilder); - // Exercise each field used by WriteOptions.equals() with a mismatch. WriteOptions gzipMismatch = new WriteOptions.Builder() .database("my-database").precision(WritePrecision.S).gzipThreshold(1024).noSync(true).build(); WriteOptions noSyncMismatch = new WriteOptions.Builder() @@ -121,6 +121,16 @@ void optionsWithTagOrder() { WriteOptions options = new WriteOptions.Builder().tagOrder(tagOrder).build(); Assertions.assertThat(options.tagOrderSafe()).containsExactly("region", "host"); + + WriteOptions optionsWithIgnoredEntries = new WriteOptions.Builder() + .tagOrder(Arrays.asList("region", null, "", "host")) + .build(); + Assertions.assertThat(optionsWithIgnoredEntries.tagOrderSafe()).containsExactly("region", "host"); + + WriteOptions ctorOptionsWithIgnoredEntries = new WriteOptions( + "my-database", WritePrecision.NS, 512, false, Map.of(), Map.of(), + Arrays.asList("region", null, "", "host")); + Assertions.assertThat(ctorOptionsWithIgnoredEntries.tagOrderSafe()).containsExactly("region", "host"); } @Test From 15a5dfdcec71f092d464101dde9f15ca68c546c4 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Mon, 2 Mar 2026 11:56:43 +0100 Subject: [PATCH 10/14] test: more coverage --- .../com/influxdb/v3/client/PointTest.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/test/java/com/influxdb/v3/client/PointTest.java b/src/test/java/com/influxdb/v3/client/PointTest.java index 887fbc2b..254169bf 100644 --- a/src/test/java/com/influxdb/v3/client/PointTest.java +++ b/src/test/java/com/influxdb/v3/client/PointTest.java @@ -206,6 +206,14 @@ void toLineProtocolWithTagOrder() { .setTag("region", "point-region") .setField("field1", 42); + String lineProtocolFastPathWithEmptyDefaults = point.toLineProtocol( + WritePrecision.NS, + Map.of(), + List.of() + ); + Assertions.assertThat("measurement,host=h1,region=point-region field1=42i") + .isEqualTo(lineProtocolFastPathWithEmptyDefaults); + String lineProtocol = point.toLineProtocol( WritePrecision.NS, Map.of("region", "default-region", "rack", "r1"), @@ -215,6 +223,22 @@ void toLineProtocolWithTagOrder() { Assertions.assertThat("measurement,region=default-region,host=h1,rack=r1 field1=42i") .isEqualTo(lineProtocol); + String lineProtocolWithTagOrderOnly = point.toLineProtocol( + WritePrecision.NS, + null, + List.of("region") + ); + Assertions.assertThat("measurement,region=point-region,host=h1 field1=42i") + .isEqualTo(lineProtocolWithTagOrderOnly); + + String lineProtocolWithDefaultTagsOnly = point.toLineProtocol( + WritePrecision.NS, + Map.of("rack", "r1"), + List.of() + ); + Assertions.assertThat("measurement,host=h1,rack=r1,region=point-region field1=42i") + .isEqualTo(lineProtocolWithDefaultTagsOnly); + Point pointWithIgnoredTags = Point.measurement("measurement") .setTag("", "ignored") .setTag("host", "h1") @@ -235,6 +259,28 @@ void toLineProtocolWithTagOrder() { Assertions.assertThat("measurement,region=point-region,host=h1,rack=r1,zone=z1 field1=42i") .isEqualTo(lineProtocolWithIgnoredTagOrderEntries); + + Point pointWithEmptyTagValue = Point.measurement("measurement") + .setTag("host", "") + .setField("field1", 42); + String lineProtocolWithEmptyTagValue = pointWithEmptyTagValue.toLineProtocol( + WritePrecision.NS, + Map.of("rack", "r1"), + List.of("host") + ); + Assertions.assertThat("measurement,rack=r1 field1=42i") + .isEqualTo(lineProtocolWithEmptyTagValue); + + Point pointWithEmptyTagNameFastPath = Point.measurement("measurement") + .setTag("", "ignored") + .setField("field1", 42); + String lineProtocolWithEmptyTagNameFastPath = pointWithEmptyTagNameFastPath.toLineProtocol( + WritePrecision.NS, + Map.of(), + List.of() + ); + Assertions.assertThat("measurement field1=42i") + .isEqualTo(lineProtocolWithEmptyTagNameFastPath); } @Test From 9a19c93067031a7970d0a89f6c06f9b8ee1e9e01 Mon Sep 17 00:00:00 2001 From: alespour <42931850+alespour@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:59:03 +0100 Subject: [PATCH 11/14] fix: precision unit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../com/influxdb/v3/client/internal/InfluxDBClientImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/influxdb/v3/client/internal/InfluxDBClientImpl.java b/src/main/java/com/influxdb/v3/client/internal/InfluxDBClientImpl.java index 2fb71864..d245df2d 100644 --- a/src/main/java/com/influxdb/v3/client/internal/InfluxDBClientImpl.java +++ b/src/main/java/com/influxdb/v3/client/internal/InfluxDBClientImpl.java @@ -336,7 +336,7 @@ private void writeData(@Nonnull final List data, @Nonnull final WriteOpti if (item == null) { return null; } else if (item instanceof Point) { - return ((Point) item).toLineProtocol(null, defaultTags, tagOrder); + return ((Point) item).toLineProtocol(precision, defaultTags, tagOrder); } else { return item.toString(); } From 581fa102abd7e628072e75335f630cb2c5799e96 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Mon, 2 Mar 2026 12:13:15 +0100 Subject: [PATCH 12/14] fix: write performance optimizations --- .../java/com/influxdb/v3/client/Point.java | 8 ++--- .../com/influxdb/v3/client/PointValues.java | 12 +++++++ .../client/internal/InfluxDBClientImpl.java | 35 ++++++++++++------- 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/influxdb/v3/client/Point.java b/src/main/java/com/influxdb/v3/client/Point.java index bbeb7895..65e2dd05 100644 --- a/src/main/java/com/influxdb/v3/client/Point.java +++ b/src/main/java/com/influxdb/v3/client/Point.java @@ -588,19 +588,17 @@ private void appendTags(@Nonnull final StringBuilder sb, @Nullable final Map defaultTags, @Nullable final List tagOrder) { if ((defaultTags == null || defaultTags.isEmpty()) && (tagOrder == null || tagOrder.isEmpty())) { - for (String name : values.getTagNames()) { - appendTag(sb, name, values.getTag(name)); - } + values.forEachTag((name, value) -> appendTag(sb, name, value)); sb.append(' '); return; } Set remaining = new TreeSet<>(); - for (String pointTag : values.getTagNames()) { + values.forEachTagName(pointTag -> { if (!pointTag.isEmpty()) { remaining.add(pointTag); } - } + }); if (defaultTags != null) { for (String defaultTag : defaultTags.keySet()) { if (defaultTag != null && !defaultTag.isEmpty()) { diff --git a/src/main/java/com/influxdb/v3/client/PointValues.java b/src/main/java/com/influxdb/v3/client/PointValues.java index 4b706dda..e7da4034 100644 --- a/src/main/java/com/influxdb/v3/client/PointValues.java +++ b/src/main/java/com/influxdb/v3/client/PointValues.java @@ -25,6 +25,8 @@ import java.time.Instant; import java.util.Map; import java.util.TreeMap; +import java.util.function.BiConsumer; +import java.util.function.Consumer; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; @@ -226,6 +228,16 @@ public String[] getTagNames() { return tags.keySet().toArray(new String[0]); } + void forEachTag(@Nonnull final BiConsumer consumer) { + Arguments.checkNotNull(consumer, "consumer"); + tags.forEach(consumer); + } + + void forEachTagName(@Nonnull final Consumer consumer) { + Arguments.checkNotNull(consumer, "consumer"); + tags.keySet().forEach(consumer); + } + /** * Gets the float field value associated with the specified name. * If the field is not present, returns null. diff --git a/src/main/java/com/influxdb/v3/client/internal/InfluxDBClientImpl.java b/src/main/java/com/influxdb/v3/client/internal/InfluxDBClientImpl.java index d245df2d..b0f884c0 100644 --- a/src/main/java/com/influxdb/v3/client/internal/InfluxDBClientImpl.java +++ b/src/main/java/com/influxdb/v3/client/internal/InfluxDBClientImpl.java @@ -32,7 +32,6 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiFunction; import java.util.logging.Logger; -import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; import java.util.zip.GZIPOutputStream; @@ -332,17 +331,27 @@ private void writeData(@Nonnull final List data, @Nonnull final WriteOpti Map defaultTags = options.defaultTagsSafe(config); List tagOrder = options.tagOrderSafe(); - String lineProtocol = data.stream().map(item -> { - if (item == null) { - return null; - } else if (item instanceof Point) { - return ((Point) item).toLineProtocol(precision, defaultTags, tagOrder); - } else { - return item.toString(); - } - }) - .filter(it -> it != null && !it.isEmpty()) - .collect(Collectors.joining("\n")); + StringBuilder lineProtocolBuilder = new StringBuilder(); + for (T item : data) { + String line; + if (item == null) { + line = null; + } else if (item instanceof Point) { + line = ((Point) item).toLineProtocol(null, defaultTags, tagOrder); + } else { + line = item.toString(); + } + + if (line == null || line.isEmpty()) { + continue; + } + + if (lineProtocolBuilder.length() > 0) { + lineProtocolBuilder.append('\n'); + } + lineProtocolBuilder.append(line); + } + String lineProtocol = lineProtocolBuilder.toString(); if (lineProtocol.isEmpty()) { LOG.warning("No data to write, please check your input data."); @@ -353,7 +362,7 @@ private void writeData(@Nonnull final List data, @Nonnull final WriteOpti byte[] body = lineProtocol.getBytes(StandardCharsets.UTF_8); if (lineProtocol.length() >= options.gzipThresholdSafe(config)) { try { - body = gzipData(lineProtocol.getBytes(StandardCharsets.UTF_8)); + body = gzipData(body); headers.put("Content-Encoding", "gzip"); } catch (IOException e) { throw new InfluxDBApiException(e); From 32bfd04e64f6edf03abc0b5560917fa36d8c13c5 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Mon, 2 Mar 2026 12:27:53 +0100 Subject: [PATCH 13/14] fix: dedup --- src/main/java/com/influxdb/v3/client/Point.java | 4 +--- src/main/java/com/influxdb/v3/client/write/WriteOptions.java | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/influxdb/v3/client/Point.java b/src/main/java/com/influxdb/v3/client/Point.java index 65e2dd05..bc907774 100644 --- a/src/main/java/com/influxdb/v3/client/Point.java +++ b/src/main/java/com/influxdb/v3/client/Point.java @@ -26,7 +26,6 @@ import java.text.NumberFormat; import java.time.Instant; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -609,9 +608,8 @@ private void appendTags(@Nonnull final StringBuilder sb, List orderedTagNames = new ArrayList<>(); if (tagOrder != null && !tagOrder.isEmpty()) { - Set seen = new HashSet<>(); for (String preferredTag : tagOrder) { - if (preferredTag == null || preferredTag.isEmpty() || !seen.add(preferredTag)) { + if (preferredTag == null || preferredTag.isEmpty()) { continue; } if (remaining.remove(preferredTag)) { diff --git a/src/main/java/com/influxdb/v3/client/write/WriteOptions.java b/src/main/java/com/influxdb/v3/client/write/WriteOptions.java index 975cde80..e9b4ca1b 100644 --- a/src/main/java/com/influxdb/v3/client/write/WriteOptions.java +++ b/src/main/java/com/influxdb/v3/client/write/WriteOptions.java @@ -354,6 +354,7 @@ private static List sanitizeTagOrder(@Nullable final List tagOrd return tagOrder.stream() .filter(Objects::nonNull) .filter(tag -> !tag.isEmpty()) + .distinct() .collect(Collectors.toUnmodifiableList()); } From 24df6b45a6a6d36a0319ded6827d3452fb28f990 Mon Sep 17 00:00:00 2001 From: alespour <42931850+alespour@users.noreply.github.com> Date: Mon, 2 Mar 2026 12:42:41 +0100 Subject: [PATCH 14/14] fix: body length for gzip evaluation Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../com/influxdb/v3/client/internal/InfluxDBClientImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/influxdb/v3/client/internal/InfluxDBClientImpl.java b/src/main/java/com/influxdb/v3/client/internal/InfluxDBClientImpl.java index b0f884c0..8c2ce466 100644 --- a/src/main/java/com/influxdb/v3/client/internal/InfluxDBClientImpl.java +++ b/src/main/java/com/influxdb/v3/client/internal/InfluxDBClientImpl.java @@ -360,7 +360,7 @@ private void writeData(@Nonnull final List data, @Nonnull final WriteOpti Map headers = new HashMap<>(Map.of("Content-Type", "text/plain; charset=utf-8")); byte[] body = lineProtocol.getBytes(StandardCharsets.UTF_8); - if (lineProtocol.length() >= options.gzipThresholdSafe(config)) { + if (body.length >= options.gzipThresholdSafe(config)) { try { body = gzipData(body); headers.put("Content-Encoding", "gzip");