From 95918d0bcf1ac3c02b26cc5fe538bac518a454ea Mon Sep 17 00:00:00 2001 From: karel rehor Date: Wed, 8 Apr 2026 10:59:27 +0200 Subject: [PATCH 1/3] chore: add selected changes from hotfix/v6.10.1 re: parseTags --- .../client/internal/InfluxQLQueryApiImpl.java | 71 +++++++- .../influxdb/client/ITInfluxQLQueryApi.java | 25 +++ .../internal/InfluxQLQueryApiImplTest.java | 167 ++++++++++++++++++ 3 files changed, 262 insertions(+), 1 deletion(-) diff --git a/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java b/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java index 8d5c7b37ec..975eaf18bc 100644 --- a/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java +++ b/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java @@ -233,7 +233,7 @@ static InfluxQLQueryResult readInfluxQLCSVResult( return new InfluxQLQueryResult(results); } - private static Map parseTags(@Nonnull final String value) { + /*private static Map parseTags(@Nonnull final String value) { final Map tags = new HashMap<>(); if (value.length() > 0) { for (String entry : value.split(",")) { @@ -242,6 +242,75 @@ private static Map parseTags(@Nonnull final String value) { } } + return tags; + } */ + + private static Map parseTags(@Nonnull final String value) { + final Map tags = new HashMap<>(); + if (value.isEmpty()) { + return tags; + } + + StringBuilder currentKey = new StringBuilder(); + StringBuilder currentValue = new StringBuilder(); + boolean inValue = false; + boolean escaped = false; + + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + + if (escaped) { + // current character is escaped - treat it as a literal + if (inValue) { + currentValue.append(c); + } else { + currentKey.append(c); + } + escaped = false; + continue; + } + + if (c == '\\') { + // start escape sequence + // don't preserve escape character + escaped = true; + continue; + } + + if (!inValue && c == '=') { + // unescaped '=' marks copula + inValue = true; + continue; + } + + if (inValue && c == ',') { + // unescaped comma separates key value pairs + // finalize + String key = currentKey.toString(); + String val = currentValue.toString(); + if (!key.isEmpty()) { + tags.put(key, val); + } + currentKey.setLength(0); + currentValue.setLength(0); + inValue = false; + continue; + } + + if (inValue) { + currentValue.append(c); + } else { + currentKey.append(c); + } + } + + // finalize last key/value pair if any + String key = currentKey.toString(); + String val = currentValue.toString(); + if (inValue && !key.isEmpty()) { + tags.put(key, val); + } + return tags; } diff --git a/client/src/test/java/com/influxdb/client/ITInfluxQLQueryApi.java b/client/src/test/java/com/influxdb/client/ITInfluxQLQueryApi.java index 9501e92e03..b7910f6f62 100644 --- a/client/src/test/java/com/influxdb/client/ITInfluxQLQueryApi.java +++ b/client/src/test/java/com/influxdb/client/ITInfluxQLQueryApi.java @@ -115,6 +115,31 @@ void testQueryData() { }); } + @Test + void testQueryWithTagsWithEscapedChars() { + Bucket bucket = influxDBClient.getBucketsApi().findBucketByName("my-bucket"); + influxDBClient.getWriteApiBlocking() + .writePoint(bucket.getId(), bucket.getOrgID(), new Point("specialTags") + .time(1655900000, WritePrecision.S) + .addField("free", 10) + .addTag("host", "A") + .addTag("region", "west") + .addTag("location", "vancouver, BC") + .addTag("model, uid","droid, C3PO") + ); + + Map expectedTags = new HashMap<>(); + expectedTags.put("host", "A"); + expectedTags.put("region", "west"); + expectedTags.put("location", "vancouver, BC"); + expectedTags.put("model, uid","droid, C3PO"); + + InfluxQLQueryResult result = influxQLQueryApi.query( + new InfluxQLQuery("SELECT * FROM \"specialTags\" GROUP BY *", DATABASE_NAME)); + + Assertions.assertThat(result.getResults().get(0).getSeries().get(0).getTags()).isEqualTo(expectedTags); + } + @Test void testQueryDataWithConversion() { InfluxQLQueryResult result = influxQLQueryApi.query( diff --git a/client/src/test/java/com/influxdb/client/internal/InfluxQLQueryApiImplTest.java b/client/src/test/java/com/influxdb/client/internal/InfluxQLQueryApiImplTest.java index 15295b731c..409ebad427 100644 --- a/client/src/test/java/com/influxdb/client/internal/InfluxQLQueryApiImplTest.java +++ b/client/src/test/java/com/influxdb/client/internal/InfluxQLQueryApiImplTest.java @@ -24,8 +24,13 @@ import java.io.IOException; import java.io.StringReader; import java.time.Instant; +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; import com.influxdb.Cancellable; import com.influxdb.query.InfluxQLQueryResult; @@ -34,6 +39,8 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import javax.annotation.Nonnull; + class InfluxQLQueryApiImplTest { private static final Cancellable NO_CANCELLING = new Cancellable() { @@ -47,6 +54,166 @@ public boolean isCancelled() { } }; + private static Map mapOf(@Nonnull final String... valuePairs) { + Map map = new HashMap<>(); + if (valuePairs.length % 2 != 0) { + throw new IllegalArgumentException("value pairs must be even"); + } + for (int i = 0; i < valuePairs.length; i += 2) { + map.put(valuePairs[i], valuePairs[i + 1]); + } + return map; + } + + private static void assertParsedTags( + @Nonnull final String rawTags, + @Nonnull final Map expectedTags + ) throws IOException { + + StringReader reader = new StringReader( + "name,tags,time,value\n" + + "m,\"" + rawTags + "\",1,1\n" + ); + InfluxQLQueryResult result = InfluxQLQueryApiImpl.readInfluxQLCSVResult(reader, NO_CANCELLING, null); + + Assertions.assertThat(result.getResults()).hasSize(1); + Assertions.assertThat(result.getResults().get(0).getSeries()).hasSize(1); + Assertions.assertThat(result.getResults().get(0).getSeries().get(0).getTags()).isEqualTo(expectedTags); + } + + @Test + void readInfluxQLResultWithMalformedAndBoundaryTagCases() throws IOException { + assertParsedTags("", mapOf()); + assertParsedTags("host=", mapOf("host", "")); + assertParsedTags("host=a,host=b", mapOf("host", "b")); + assertParsedTags("host=a,broken", mapOf("host", "a")); + assertParsedTags("=a,host=b", mapOf("host", "b")); + assertParsedTags("host=a,", mapOf("host", "a")); + assertParsedTags(",host=a", mapOf(",host", "a")); + assertParsedTags("a=1,,b=2", mapOf("a", "1", ",b", "2")); + assertParsedTags("a=foo\\", mapOf("a", "foo")); + assertParsedTags("k\\==v\\=1", mapOf("k=", "v=1")); + assertParsedTags("k\\,x=v\\,y,b=2", mapOf("k,x", "v,y", "b", "2")); + assertParsedTags("k\\=x", mapOf()); + } + + @Test + void readInfluxQLResultWithTagCommas() throws IOException { + InfluxQLQueryResult.Series.ValueExtractor extractValue = (columnName, rawValue, resultIndex, seriesName) -> { + if (resultIndex == 0 && seriesName.equals("data1")){ + switch (columnName){ + case "time": return Instant.ofEpochSecond(Long.parseLong(rawValue)); + case "first": + return Double.valueOf(rawValue); + } + } + return rawValue; + }; + + // Note that escapes in tags returned from server are themselves escaped + List testTags = Arrays.asList( + "location=Cheb_CZ", //simpleTag + "region=us-east-1,host=server1", // standardTags * 2 + "location=Cheb\\,\\ CZ", // simpleTag with value comma and space + "location=Cheb_CZ,branch=Munchen_DE", // multiple tags with underscore + "location=Cheb\\,\\ CZ,branch=Munchen\\,\\ DE", // multiple tags with comma and space + "model\\,\\ uin=C3PO", // tag with comma space in key + "model\\,\\ uin=Droid\\,\\ C3PO", // tag with comma space in key and value + "model\\,\\ uin=Droid\\,\\ C3PO,location=Cheb\\,\\ CZ,branch=Munchen\\,\\ DE", // comma space in key and val + "silly\\,\\=long\\,tag=a\\,b\\\\\\,\\ c\\,\\ d", // multi commas in k and v plus escaped reserved chars + "region=us\\,\\ east-1,host\\,\\ name=ser\\,\\ ver1" // legacy broken tags + ); + + Map> expectedTagsMap = Stream.of( + // 1. simpleTag + new AbstractMap.SimpleImmutableEntry<>(testTags.get(0), + mapOf("location", "Cheb_CZ")), + // 2. standardTags * 2 + new AbstractMap.SimpleImmutableEntry<>(testTags.get(1), + mapOf( + "region", "us-east-1", + "host", "server1" + )), + // 3. simpleTag with value comma and space + new AbstractMap.SimpleImmutableEntry<>(testTags.get(2), + mapOf("location", "Cheb, CZ")), + // 4. multiple tags with underscore + new AbstractMap.SimpleImmutableEntry<>(testTags.get(3), + mapOf( + "location", "Cheb_CZ", + "branch", "Munchen_DE" + )), + // 5. multiple tags with comma and space + new AbstractMap.SimpleImmutableEntry<>(testTags.get(4), + mapOf( + "location", "Cheb, CZ", + "branch", "Munchen, DE" + )), + // 6. tag with comma and space in key + new AbstractMap.SimpleImmutableEntry<>(testTags.get(5), + mapOf("model, uin", "C3PO")), + // 7. tag with comma and space in key and value + new AbstractMap.SimpleImmutableEntry<>(testTags.get(6), + mapOf("model, uin", "Droid, C3PO")), + // 8. comma space in key and val with multiple tags + new AbstractMap.SimpleImmutableEntry<>(testTags.get(7), + mapOf( + "model, uin", "Droid, C3PO", + "location", "Cheb, CZ", + "branch", "Munchen, DE" + )), + // 9. multiple commas in key and value + new AbstractMap.SimpleImmutableEntry<>(testTags.get(8), + mapOf( + "silly,=long,tag", "a,b\\, c, d" + )), + // legacy broken tags + new AbstractMap.SimpleImmutableEntry<>(testTags.get(9), + mapOf( + "region", "us, east-1", + "host, name", "ser, ver1" + )) + ).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + StringReader reader = new StringReader("name,tags,time,first\n" + + "data1,\"" + testTags.get(0) + "\",1483225200,42\n" + + "data1,\"" + testTags.get(1) + "\",1483225200,42\n" + + "data1,\"" + testTags.get(2) + "\",1483225200,42\n" + + "data1,\"" + testTags.get(3) + "\",1483225200,42\n" + + "data1,\"" + testTags.get(4) + "\",1483225200,42\n" + + "data1,\"" + testTags.get(5) + "\",1483225200,42\n" + + "data1,\"" + testTags.get(6) + "\",1483225200,42\n" + + "data1,\"" + testTags.get(7) + "\",1483225200,42\n" + + "data1,\"" + testTags.get(8) + "\",1483225200,42\n" + + "\n" + + "name,tags,time,usage_user,usage_system\n" + + "cpu,\"" + testTags.get(9) + "\",1483225200,13.57,1.4\n" + ); + + InfluxQLQueryResult result = InfluxQLQueryApiImpl.readInfluxQLCSVResult(reader, NO_CANCELLING, extractValue); + List results = result.getResults(); + int index = 0; + for(InfluxQLQueryResult.Result r : results) { + for(InfluxQLQueryResult.Series s : r.getSeries()){ + Assertions.assertThat(s.getTags()).isEqualTo(expectedTagsMap.get(testTags.get(index++))); + if(index < 10) { + Assertions.assertThat(s.getColumns()).containsOnlyKeys("time", "first"); + InfluxQLQueryResult.Series.Record valRec = s.getValues().get(0); + Assertions.assertThat(valRec.getValueByKey("first")).isEqualTo(Double.valueOf("42.0")); + Assertions.assertThat(valRec.getValueByKey("time")).isEqualTo(Instant.ofEpochSecond(1483225200L)); + } else if (index == 10) { + Assertions.assertThat(s.getColumns()).containsOnlyKeys("time", "usage_user", "usage_system"); + InfluxQLQueryResult.Series.Record valRec = s.getValues().get(0); + // No value extractor created for "cpu" series + Assertions.assertThat(valRec.getValueByKey("time")).isEqualTo("1483225200"); + Assertions.assertThat(valRec.getValueByKey("usage_user")).isEqualTo("13.57"); + Assertions.assertThat(valRec.getValueByKey("usage_system")).isEqualTo("1.4"); + } + } + } + Assertions.assertThat(index).isEqualTo(testTags.size()); + } + @Test void readInfluxQLResult() throws IOException { InfluxQLQueryResult.Series.ValueExtractor extractValues = (columnName, rawValue, resultIndex, seriesName) -> { From 907f081ada6161744b28940dc78e414373e4aba8 Mon Sep 17 00:00:00 2001 From: karel rehor Date: Wed, 8 Apr 2026 13:11:43 +0200 Subject: [PATCH 2/3] chore: remove commented code --- .../client/internal/InfluxQLQueryApiImpl.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java b/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java index 975eaf18bc..85e11fdd98 100644 --- a/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java +++ b/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java @@ -233,18 +233,6 @@ static InfluxQLQueryResult readInfluxQLCSVResult( return new InfluxQLQueryResult(results); } - /*private static Map parseTags(@Nonnull final String value) { - final Map tags = new HashMap<>(); - if (value.length() > 0) { - for (String entry : value.split(",")) { - final String[] kv = entry.split("="); - tags.put(kv[0], kv[1]); - } - } - - return tags; - } */ - private static Map parseTags(@Nonnull final String value) { final Map tags = new HashMap<>(); if (value.isEmpty()) { From d05e87655c1d7a3514edee13fc51b33cc55c38ad Mon Sep 17 00:00:00 2001 From: karel rehor Date: Wed, 8 Apr 2026 13:52:55 +0200 Subject: [PATCH 3/3] docs: update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ec08bd977..1e6298461e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - [#850](https://github.com/influxdata/influxdb-client-java/pull/850): Update the Spring health indicator integration to use the Spring Boot 4 health APIs. +### Bug Fixes + +1. [#856](https://github.com/influxdata/influxdb-client-java/pull/865): with InfluxQL queries parse escaped special characters in tag keys and values e.g. (`"my_data,model\,\ uin=Droid\,\ C3PO ..."`) + ### Dependencies Update dependencies: