Skip to content
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
//
Expand Down
85 changes: 73 additions & 12 deletions src/main/java/com/influxdb/v3/client/Point.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,13 @@
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.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;
Expand Down Expand Up @@ -530,7 +535,7 @@ public Point copy() {
*/
@Nonnull
public String toLineProtocol() {
return toLineProtocol(null);
return toLineProtocol(null, null, null);
}

/**
Expand All @@ -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<String, String> defaultTags,
@Nullable final List<String> tagOrder) {

StringBuilder sb = new StringBuilder();

escapeKey(sb, getMeasurement(), false);
appendTags(sb);
appendTags(sb, defaultTags, tagOrder);
boolean appendedFields = appendFields(sb);
if (!appendedFields) {
return "";
Expand All @@ -564,24 +584,65 @@ private Point putField(@Nonnull final String field, @Nullable final Object value
return this;
}

private void appendTags(@Nonnull final StringBuilder sb) {

for (String name : values.getTagNames()) {
private void appendTags(@Nonnull final StringBuilder sb,
@Nullable final Map<String, String> defaultTags,
@Nullable final List<String> tagOrder) {
if ((defaultTags == null || defaultTags.isEmpty()) && (tagOrder == null || tagOrder.isEmpty())) {
for (String name : values.getTagNames()) {
appendTag(sb, name, values.getTag(name));
}
sb.append(' ');
return;
}

String value = values.getTag(name);
Set<String> remaining = new TreeSet<>();
for (String pointTag : values.getTagNames()) {
if (!pointTag.isEmpty()) {
remaining.add(pointTag);
}
}
if (defaultTags != null) {
for (String defaultTag : defaultTags.keySet()) {
if (defaultTag != null && !defaultTag.isEmpty()) {
remaining.add(defaultTag);
}
}
}

if (name.isEmpty() || value == null || value.isEmpty()) {
continue;
List<String> orderedTagNames = new ArrayList<>();
if (tagOrder != null && !tagOrder.isEmpty()) {
Set<String> 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);

sb.append(',');
escapeKey(sb, name);
sb.append('=');
escapeKey(sb, value);
for (String name : orderedTagNames) {
String value = values.getTag(name);
if (defaultTags != null && defaultTags.containsKey(name)) {
value = defaultTags.get(name);
}
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,15 +330,13 @@ private <T> void writeData(@Nonnull final List<T> data, @Nonnull final WriteOpti
}

Map<String, String> defaultTags = options.defaultTagsSafe(config);
List<String> 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(precision, defaultTags, tagOrder);
} else {
return item.toString();
}
Expand Down
75 changes: 71 additions & 4 deletions src/main/java/com/influxdb/v3/client/write/WriteOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
package com.influxdb.v3.client.write;

import java.util.HashMap;
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;
Expand All @@ -40,6 +42,7 @@
* <li><code>organization</code> - specifies the organization to be used for InfluxDB operations</li>
* <li><code>precision</code> - specifies the precision to use for the timestamp of points</li>
* <li><code>defaultTags</code> - specifies tags to be added by default to all write operations using points.</li>
* <li><code>tagOrder</code> - specifies preferred tag order for point serialization.</li>
* <li><code>headers</code> - specifies the headers to be added to write request</li>
* </ul>
* <p>
Expand Down Expand Up @@ -78,13 +81,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<String, String> defaultTags;
private final List<String> tagOrder;
private final Map<String, String> headers;

/**
Expand All @@ -94,7 +98,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);
}

/**
Expand Down Expand Up @@ -204,11 +209,40 @@ public WriteOptions(@Nullable final String database,
@Nullable final Boolean noSync,
@Nullable final Map<String, String> defaultTags,
@Nullable final Map<String, String> 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.
* Null or empty tag names are ignored.
*/
public WriteOptions(@Nullable final String database,
@Nullable final WritePrecision precision,
@Nullable final Integer gzipThreshold,
@Nullable final Boolean noSync,
@Nullable final Map<String, String> defaultTags,
@Nullable final Map<String, String> headers,
@Nullable final List<String> tagOrder) {
this.database = database;
this.precision = precision;
this.gzipThreshold = gzipThreshold;
this.noSync = noSync;
this.defaultTags = defaultTags == null ? Map.of() : defaultTags;
this.tagOrder = sanitizeTagOrder(tagOrder);
this.headers = headers == null ? Map.of() : headers;
}

Expand Down Expand Up @@ -277,6 +311,14 @@ public Map<String, String> headersSafe() {
return headers;
}

/**
* @return preferred order of tags in line protocol serialization.
*/
@Nonnull
public List<String> tagOrderSafe() {
return tagOrder;
}

@Override
public boolean equals(final Object o) {
if (this == o) {
Expand All @@ -291,18 +333,30 @@ 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) {
return option == null || option.isEmpty();
}

@Nonnull
private static List<String> sanitizeTagOrder(@Nullable final List<String> 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}.
* <p>
Expand All @@ -314,6 +368,7 @@ public static final class Builder {
private Integer gzipThreshold;
private Boolean noSync;
private Map<String, String> defaultTags = new HashMap<>();
private List<String> tagOrder = List.of();
private Map<String, String> headers = new HashMap<>();

/**
Expand Down Expand Up @@ -380,6 +435,18 @@ public Builder defaultTags(@Nonnull final Map<String, String> defaultTags) {
return this;
}

/**
* Sets preferred tag order for line protocol serialization.
*
* @param tagOrder tag order preference. Null or empty tag names are ignored.
* @return this
*/
@Nonnull
public Builder tagOrder(@Nonnull final List<String> tagOrder) {
this.tagOrder = sanitizeTagOrder(tagOrder);
return this;
}

/**
* Sets the headers.
*
Expand All @@ -406,6 +473,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);
}
}
19 changes: 19 additions & 0 deletions src/test/java/com/influxdb/v3/client/InfluxDBClientWriteTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading