From 7c0d602d2ba1e36fca3b550ed1bea9985a1e269f Mon Sep 17 00:00:00 2001 From: karel rehor Date: Fri, 13 Mar 2026 17:59:37 +0100 Subject: [PATCH 01/13] fix: (WIP) ensure tag keys and values with commas can be parsed correctly. --- .../client/internal/InfluxQLQueryApiImpl.java | 93 ++++++++++++++++++- .../internal/InfluxQLQueryApiImplTest.java | 54 +++++++++++ 2 files changed, 145 insertions(+), 2 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 f02f6d97e9..f99168f42a 100644 --- a/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java +++ b/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java @@ -120,8 +120,14 @@ static InfluxQLQueryResult readInfluxQLResult( // All other columns are dynamically parsed final int dynamicColumnsStartIndex = 2; - try (CSVParser parser = new CSVParser(reader, CSVFormat.DEFAULT.builder().setIgnoreEmptyLines(false).build())) { + // CSVFormat.RFC4180.builder() + try (CSVParser parser = new CSVParser(reader, CSVFormat.DEFAULT.builder() + // .setEscape('\\') + .setIgnoreEmptyLines(false) + // .setTrim(true) + .build())) { for (CSVRecord csvRecord : parser) { + // System.out.println("DEBUG csvRecord: " + csvRecord + ", size " + csvRecord.size()); if (cancellable.isCancelled()) { break; } @@ -156,8 +162,11 @@ static InfluxQLQueryResult readInfluxQLResult( Arrays.asList(name, finalTags), n -> new InfluxQLQueryResult.Series(name, finalTags, finalHeaderCols) ); + // System.out.println("DEBUG finalHeaderCols: " + finalHeaderCols + " dynamiColumnsStartIndex: " + dynamicColumnsStartIndex); + //getCSVField(csvRecord, headerCols.get("time") + dynamicColumnsStartIndex); Object[] values = headerCols.entrySet().stream().map(entry -> { - String value = csvRecord.get(entry.getValue() + dynamicColumnsStartIndex); + // String value = csvRecord.get(entry.getValue() + dynamicColumnsStartIndex); + String value = getCSVField(csvRecord, entry.getValue() + dynamicColumnsStartIndex); if (valueExtractor != null) { return valueExtractor.extractValue(entry.getKey(), value, resultIndex, serie.getName()); } @@ -178,6 +187,84 @@ static InfluxQLQueryResult readInfluxQLResult( return new InfluxQLQueryResult(results); } + // Need to fixup any fields that might have contained a commented comma + private static String getCSVField(CSVRecord record, int col_index) { + ArrayList fixupValues = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < record.size(); i++) { + // System.out.println("DEBUG record.values()["+ i + "]: " + record.values()[i]); + if(record.values()[i].endsWith("\\")){ + sb.append(record.get(i)).append(","); + } else { + sb.append(record.get(i)); + fixupValues.add(sb.toString()); + sb.delete(0, sb.length()); + } + } + // System.out.println("DEBUG fixupValues: " + fixupValues); + // System.out.println("DEBUG co_index: " + col_index + " fixupValues.size: " + fixupValues.size()); + // System.out.println("DEBUG result: " + fixupValues.get(col_index)); + return fixupValues.get(col_index); + } + + private static int IndexOfUnescapedChar(String str, char ch) { + char[] chars = str.toCharArray(); + for (int i = 0; i < chars.length; i++) { + if (chars[i] == ch && chars[i-1] != '\\') { + return i; + } + } + return -1; + } + + private static Map parseTags(@Nonnull final String value) { + final Map tags = new HashMap<>(); + final List keys = new ArrayList<>(); + final List values = new ArrayList<>(); + // System.out.println("DEBUG ARG value: " + value); + if (!value.isEmpty()) { + String[] chunks = value.split("="); + for (int i = 0; i < chunks.length; i++) { + // System.out.println("DEBUG chunks[" + i + "]: " + chunks[i]); + if (i == 0) { + keys.add(chunks[i]); + } else if (i == chunks.length - 1) { + values.add(chunks[i]); + } else { + int comma_index = IndexOfUnescapedChar(chunks[i], ','); + if (comma_index != -1) { + String v = chunks[i].substring(0, comma_index); + String k = chunks[i].substring(comma_index + 1); + // System.out.println("DEBUG v: " + v + " k: " + k); + keys.add(k); + values.add(v); + } + } + } + // System.out.println("DEBUG keys: "); + // for(String key : keys) { + // System.out.println(" key: " + key); + //} + // System.out.println("DEBUG values: "); + //for(String val : values) { + // System.out.println(" val: " + val); + //} + for (int i = 0; i < keys.size(); i++) { + // tags.put(keys.get(i), values.get(i)); + tags.put( + keys.get(i).contains("\\,") ? "\"" + keys.get(i) + "\"" : keys.get(i), + values.get(i).contains("\\,") ? "\"" + values.get(i) + "\"" : values.get(i) + ); + } + } + // System.out.println("DEBUG tags: "); + // for(String key : tags.keySet()) { + // System.out.println(" tags[" + key + "]: " + tags.get(key)); + // } + return tags; + } + + /* private static Map parseTags(@Nonnull final String value) { final Map tags = new HashMap<>(); if (value.length() > 0) { @@ -189,4 +276,6 @@ private static Map parseTags(@Nonnull final String value) { return tags; } + + */ } 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 0f7f94bf1a..0987490f87 100644 --- a/client/src/test/java/com/influxdb/client/internal/InfluxQLQueryApiImplTest.java +++ b/client/src/test/java/com/influxdb/client/internal/InfluxQLQueryApiImplTest.java @@ -24,12 +24,17 @@ import java.io.IOException; import java.io.StringReader; import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import com.influxdb.Cancellable; import com.influxdb.query.InfluxQLQueryResult; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.platform.engine.TestTag; class InfluxQLQueryApiImplTest { @@ -44,6 +49,55 @@ public boolean isCancelled() { } }; + @Test + void readInfluxQLResultWithTagCommas() throws IOException { + InfluxQLQueryResult.Series.ValueExtractor extractValue = (columnName, rawValue, resultIndex, seriesName) -> { + // System.out.println("DEBUG columnName: " + columnName + ", rawValue: " + rawValue + ", resultIndex: " + resultIndex + ", seriesName: " + seriesName); + if (resultIndex == 0 && seriesName.equals("data1")){ + switch (columnName){ + case "time": return Instant.ofEpochSecond(Long.parseLong(rawValue)); + case "first": return Double.valueOf(rawValue); + //case "tags": return rawValue; + } + } + return rawValue; + }; + + // Cheb,CZ should be \"Cheb,CZ\" a single tag value + // double quotes should work - from raw sample results commas should always be escaped + StringReader reader = new StringReader("name,tags,time,first\n" + + "data1,\"location=Cheb_CZ\",1483225200,42\n" + + "data1,\"region=us-east-1,host=server1\",1483225200,13.57\n" + // + "data1,\"location=Cheb,CZ\",1483225200,42\n" // invalid - comma in value should be escaped + // + "data1,\"location=Cheb, CZ\",1483225200,42\n" // invalid - comma and space in value should be escaped + + "data1,\"location=Cheb\\,\\ CZ\",1483225200,42\n" + + "data1,\"location=Cheb_CZ,branch=Munchen_DE\",1483225200,42\n" + + "data1,\"location=Cheb\\,\\ CZ,branch=Munchen\\,\\ DE\",1483225200,42\n" + + "data1,\"model\\,\\ uin=C3PO\",1483225200,42\n" + + "data1,\"model\\,\\ uin=Droid\\, C3PO\",1483225200,42\n" + + "data1,\"location=Cheb\\,\\ CZ,branch=Munchen\\,\\ DE\",1483225200,42\n" + + "data1,\"model\\,\\ uin=Droid\\,\\ C3PO,location=Cheb\\,\\ CZ,branch=Munchen\\,\\ DE\",1483225200,42\n" + + "data1,\"silly\\,long\\,tag=a\\,b\\,\\ c\\,\\ d\",1483225200,42\n" + + "\n" + + "name,tags,time,usage_user,usage_system\n" + + "cpu,\"region=us\\,\\ east-1,host\\,\\ name=ser\\,\\ ver1\",1483225200,13.57,1.4\n" + ); + + // TODO meaningful asserts + InfluxQLQueryResult result = InfluxQLQueryApiImpl.readInfluxQLResult(reader, NO_CANCELLING, extractValue); + List results = result.getResults(); + // System.out.println("DEBUG results\n" + results.get(0).getSeries().get(0).getValues().get(0).getValueByKey("tags")); + } + + /* + Sample response 1 - note escaped commas +name,tags,time,fVal,iVal,id,location,"location\,boo",model,"model\,uin",sVal +zaphrod_b,,1773307528202967039,26.54671,-6922649068284626682,bar,Harfa,,R2D2,,FOO +zaphrod_b,,1773322199131651270,26.54671,-6922649068284626682,bar,,Harfa,R2D2,,FOO +zaphrod_b,,1773322228235655514,26.54671,-6922649068284626682,bar,,"Harfa\,\ Praha",R2D2,,FOO +zaphrod_b,,1773322254827374192,26.54671,-6922649068284626682,bar,,"Harfa\,\ Praha",,R2D2,FOO + */ + @Test void readInfluxQLResult() throws IOException { InfluxQLQueryResult.Series.ValueExtractor extractValues = (columnName, rawValue, resultIndex, seriesName) -> { From 03c959b24252208f59a17d56e684ce8a92e8710f Mon Sep 17 00:00:00 2001 From: karel rehor Date: Mon, 16 Mar 2026 18:17:37 +0100 Subject: [PATCH 02/13] chore: remove unnecessary comma parsing method --- .../client/internal/InfluxQLQueryApiImpl.java | 25 +------------------ 1 file changed, 1 insertion(+), 24 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 f99168f42a..0ed75c344e 100644 --- a/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java +++ b/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java @@ -162,11 +162,8 @@ static InfluxQLQueryResult readInfluxQLResult( Arrays.asList(name, finalTags), n -> new InfluxQLQueryResult.Series(name, finalTags, finalHeaderCols) ); - // System.out.println("DEBUG finalHeaderCols: " + finalHeaderCols + " dynamiColumnsStartIndex: " + dynamicColumnsStartIndex); - //getCSVField(csvRecord, headerCols.get("time") + dynamicColumnsStartIndex); Object[] values = headerCols.entrySet().stream().map(entry -> { - // String value = csvRecord.get(entry.getValue() + dynamicColumnsStartIndex); - String value = getCSVField(csvRecord, entry.getValue() + dynamicColumnsStartIndex); + String value = csvRecord.get(entry.getValue() + dynamicColumnsStartIndex); if (valueExtractor != null) { return valueExtractor.extractValue(entry.getKey(), value, resultIndex, serie.getName()); } @@ -187,26 +184,6 @@ static InfluxQLQueryResult readInfluxQLResult( return new InfluxQLQueryResult(results); } - // Need to fixup any fields that might have contained a commented comma - private static String getCSVField(CSVRecord record, int col_index) { - ArrayList fixupValues = new ArrayList<>(); - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < record.size(); i++) { - // System.out.println("DEBUG record.values()["+ i + "]: " + record.values()[i]); - if(record.values()[i].endsWith("\\")){ - sb.append(record.get(i)).append(","); - } else { - sb.append(record.get(i)); - fixupValues.add(sb.toString()); - sb.delete(0, sb.length()); - } - } - // System.out.println("DEBUG fixupValues: " + fixupValues); - // System.out.println("DEBUG co_index: " + col_index + " fixupValues.size: " + fixupValues.size()); - // System.out.println("DEBUG result: " + fixupValues.get(col_index)); - return fixupValues.get(col_index); - } - private static int IndexOfUnescapedChar(String str, char ch) { char[] chars = str.toCharArray(); for (int i = 0; i < chars.length; i++) { From 0a5e4e997e23ccb6f95f736dd01b625ff4b85b2f Mon Sep 17 00:00:00 2001 From: karel rehor Date: Tue, 17 Mar 2026 17:05:11 +0100 Subject: [PATCH 03/13] chore: add new test asserts, clean and lint code. --- .../client/internal/InfluxQLQueryApiImpl.java | 33 +---- .../internal/InfluxQLQueryApiImplTest.java | 121 ++++++++++++++---- 2 files changed, 106 insertions(+), 48 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 0ed75c344e..d5cac59939 100644 --- a/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java +++ b/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java @@ -120,14 +120,10 @@ static InfluxQLQueryResult readInfluxQLResult( // All other columns are dynamically parsed final int dynamicColumnsStartIndex = 2; - // CSVFormat.RFC4180.builder() try (CSVParser parser = new CSVParser(reader, CSVFormat.DEFAULT.builder() - // .setEscape('\\') .setIgnoreEmptyLines(false) - // .setTrim(true) .build())) { for (CSVRecord csvRecord : parser) { - // System.out.println("DEBUG csvRecord: " + csvRecord + ", size " + csvRecord.size()); if (cancellable.isCancelled()) { break; } @@ -184,10 +180,10 @@ static InfluxQLQueryResult readInfluxQLResult( return new InfluxQLQueryResult(results); } - private static int IndexOfUnescapedChar(String str, char ch) { + private static int indexOfUnescapedChar(@Nonnull final String str, final char ch) { char[] chars = str.toCharArray(); for (int i = 0; i < chars.length; i++) { - if (chars[i] == ch && chars[i-1] != '\\') { + if (chars[i] == ch && chars[i - 1] != '\\') { return i; } } @@ -198,46 +194,31 @@ private static Map parseTags(@Nonnull final String value) { final Map tags = new HashMap<>(); final List keys = new ArrayList<>(); final List values = new ArrayList<>(); - // System.out.println("DEBUG ARG value: " + value); if (!value.isEmpty()) { String[] chunks = value.split("="); for (int i = 0; i < chunks.length; i++) { - // System.out.println("DEBUG chunks[" + i + "]: " + chunks[i]); if (i == 0) { keys.add(chunks[i]); } else if (i == chunks.length - 1) { values.add(chunks[i]); } else { - int comma_index = IndexOfUnescapedChar(chunks[i], ','); - if (comma_index != -1) { - String v = chunks[i].substring(0, comma_index); - String k = chunks[i].substring(comma_index + 1); - // System.out.println("DEBUG v: " + v + " k: " + k); + int commaIndex = indexOfUnescapedChar(chunks[i], ','); + if (commaIndex != -1) { + String v = chunks[i].substring(0, commaIndex); + String k = chunks[i].substring(commaIndex + 1); keys.add(k); values.add(v); } } } - // System.out.println("DEBUG keys: "); - // for(String key : keys) { - // System.out.println(" key: " + key); - //} - // System.out.println("DEBUG values: "); - //for(String val : values) { - // System.out.println(" val: " + val); - //} for (int i = 0; i < keys.size(); i++) { - // tags.put(keys.get(i), values.get(i)); tags.put( keys.get(i).contains("\\,") ? "\"" + keys.get(i) + "\"" : keys.get(i), values.get(i).contains("\\,") ? "\"" + values.get(i) + "\"" : values.get(i) ); } } - // System.out.println("DEBUG tags: "); - // for(String key : tags.keySet()) { - // System.out.println(" tags[" + key + "]: " + tags.get(key)); - // } + return tags; } 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 0987490f87..64bfb89ee6 100644 --- a/client/src/test/java/com/influxdb/client/internal/InfluxQLQueryApiImplTest.java +++ b/client/src/test/java/com/influxdb/client/internal/InfluxQLQueryApiImplTest.java @@ -24,17 +24,18 @@ import java.io.IOException; import java.io.StringReader; import java.time.Instant; -import java.util.ArrayList; +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; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; -import org.junit.platform.engine.TestTag; class InfluxQLQueryApiImplTest { @@ -52,41 +53,117 @@ public boolean isCancelled() { @Test void readInfluxQLResultWithTagCommas() throws IOException { InfluxQLQueryResult.Series.ValueExtractor extractValue = (columnName, rawValue, resultIndex, seriesName) -> { - // System.out.println("DEBUG columnName: " + columnName + ", rawValue: " + rawValue + ", resultIndex: " + resultIndex + ", seriesName: " + seriesName); if (resultIndex == 0 && seriesName.equals("data1")){ switch (columnName){ case "time": return Instant.ofEpochSecond(Long.parseLong(rawValue)); - case "first": return Double.valueOf(rawValue); - //case "tags": return rawValue; + case "first": + return Double.valueOf(rawValue); } } return rawValue; }; - // Cheb,CZ should be \"Cheb,CZ\" a single tag value - // double quotes should work - from raw sample results commas should always be 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", // multiple commas in key and value + "region=us\\,\\ east-1,host\\,\\ name=ser\\,\\ ver1" // legacy broken tags + ); + + Map> expectedTagsMap = Stream.of( + // 1. simpleTag + new AbstractMap.SimpleImmutableEntry<>(testTags.get(0), + new HashMap() {{ + put("location", "Cheb_CZ"); + }}), + // 2. standardTags * 2 + new AbstractMap.SimpleImmutableEntry<>(testTags.get(1), + new HashMap() {{ + put("region", "us-east-1"); + put("host", "server1"); + }}), + // 3. simpleTag with value comma and space + new AbstractMap.SimpleImmutableEntry<>(testTags.get(2), + new HashMap() {{ + put("location", "\"Cheb\\,\\ CZ\""); + }}), + // 4. multiple tags with underscore + new AbstractMap.SimpleImmutableEntry<>(testTags.get(3), + new HashMap() {{ + put("location", "Cheb_CZ"); + put("branch", "Munchen_DE"); + }}), + // 5. multiple tags with comma and space + new AbstractMap.SimpleImmutableEntry<>(testTags.get(4), + new HashMap() {{ + put("location", "\"Cheb\\,\\ CZ\""); + put("branch", "\"Munchen\\,\\ DE\""); + }}), + // 6. tag with comma and space in key + new AbstractMap.SimpleImmutableEntry<>(testTags.get(5), + new HashMap() {{ + put("\"model\\,\\ uin\"", "C3PO"); + }}), + // 7. tag with comma and space in key and value + new AbstractMap.SimpleImmutableEntry<>(testTags.get(6), + new HashMap() {{ + put("\"model\\,\\ uin\"", "\"Droid\\, C3PO\""); + }}), + // 8. comma space in key and val with multiple tags + new AbstractMap.SimpleImmutableEntry<>(testTags.get(7), + new HashMap() {{ + put("\"model\\,\\ uin\"", "\"Droid\\,\\ C3PO\""); + put("location", "\"Cheb\\,\\ CZ\""); + put("branch", "\"Munchen\\,\\ DE\""); + }}), + // 9. multiple commas in key and value + new AbstractMap.SimpleImmutableEntry<>(testTags.get(8), + new HashMap() {{ + put("\"silly\\,long\\,tag\"", "\"a\\,b\\,\\ c\\,\\ d\""); + }}), + // legacy broken tags + new AbstractMap.SimpleImmutableEntry<>(testTags.get(9), + new HashMap() {{ + put("region", "\"us\\,\\ east-1\""); + put("\"host\\,\\ name\"", "\"ser\\,\\ ver1\""); + }}) + ).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + StringReader reader = new StringReader("name,tags,time,first\n" - + "data1,\"location=Cheb_CZ\",1483225200,42\n" - + "data1,\"region=us-east-1,host=server1\",1483225200,13.57\n" - // + "data1,\"location=Cheb,CZ\",1483225200,42\n" // invalid - comma in value should be escaped - // + "data1,\"location=Cheb, CZ\",1483225200,42\n" // invalid - comma and space in value should be escaped - + "data1,\"location=Cheb\\,\\ CZ\",1483225200,42\n" - + "data1,\"location=Cheb_CZ,branch=Munchen_DE\",1483225200,42\n" - + "data1,\"location=Cheb\\,\\ CZ,branch=Munchen\\,\\ DE\",1483225200,42\n" - + "data1,\"model\\,\\ uin=C3PO\",1483225200,42\n" - + "data1,\"model\\,\\ uin=Droid\\, C3PO\",1483225200,42\n" - + "data1,\"location=Cheb\\,\\ CZ,branch=Munchen\\,\\ DE\",1483225200,42\n" - + "data1,\"model\\,\\ uin=Droid\\,\\ C3PO,location=Cheb\\,\\ CZ,branch=Munchen\\,\\ DE\",1483225200,42\n" - + "data1,\"silly\\,long\\,tag=a\\,b\\,\\ c\\,\\ d\",1483225200,42\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,\"region=us\\,\\ east-1,host\\,\\ name=ser\\,\\ ver1\",1483225200,13.57,1.4\n" + + "cpu,\"" + testTags.get(9) + "\",1483225200,13.57,1.4\n" ); - // TODO meaningful asserts InfluxQLQueryResult result = InfluxQLQueryApiImpl.readInfluxQLResult(reader, NO_CANCELLING, extractValue); List results = result.getResults(); - // System.out.println("DEBUG results\n" + results.get(0).getSeries().get(0).getValues().get(0).getValueByKey("tags")); + 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 < 9) { + 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)); + } + } + } } /* From 970f47eab3b634f535cb834067e15fe6ea2c9901 Mon Sep 17 00:00:00 2001 From: karel rehor Date: Wed, 18 Mar 2026 13:52:02 +0100 Subject: [PATCH 04/13] chore: remove commented code block --- .../client/internal/InfluxQLQueryApiImpl.java | 15 --------------- 1 file changed, 15 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 d5cac59939..6fb2bbeeab 100644 --- a/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java +++ b/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java @@ -221,19 +221,4 @@ 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.length() > 0) { - for (String entry : value.split(",")) { - final String[] kv = entry.split("="); - tags.put(kv[0], kv[1]); - } - } - - return tags; - } - - */ } From 20269f04158a0b39b4ce26a703f575f1b41e2ddf Mon Sep 17 00:00:00 2001 From: karel rehor Date: Wed, 18 Mar 2026 14:42:23 +0100 Subject: [PATCH 05/13] chore: document logic of parseTags method. --- .../client/internal/InfluxQLQueryApiImpl.java | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 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 6fb2bbeeab..becd1e5dae 100644 --- a/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java +++ b/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java @@ -190,6 +190,14 @@ private static int indexOfUnescapedChar(@Nonnull final String str, final char ch return -1; } + /* + This works on the principle that the copula '=' is the _governing verb_ of any key to value + expression. So parsing begins based on the verb ('=') not on the assumed expression termination + character (','). The Left and right values of the split based on ('=') are collected and checked for + the correct statement terminator (an unescaped ','). Any value on the left of an unescaped ',' is a + value. Any value on the right is a key. These are placed in their respective ordered lists and then + recombined into the tags HashMap. + */ private static Map parseTags(@Nonnull final String value) { final Map tags = new HashMap<>(); final List keys = new ArrayList<>(); @@ -197,11 +205,11 @@ private static Map parseTags(@Nonnull final String value) { if (!value.isEmpty()) { String[] chunks = value.split("="); for (int i = 0; i < chunks.length; i++) { - if (i == 0) { + if (i == 0) { // first element will be a key on its own. keys.add(chunks[i]); - } else if (i == chunks.length - 1) { + } else if (i == chunks.length - 1) { // the last element will be a value on its own. values.add(chunks[i]); - } else { + } else { // check for legitimate keys and values int commaIndex = indexOfUnescapedChar(chunks[i], ','); if (commaIndex != -1) { String v = chunks[i].substring(0, commaIndex); @@ -212,9 +220,9 @@ private static Map parseTags(@Nonnull final String value) { } } for (int i = 0; i < keys.size(); i++) { - tags.put( - keys.get(i).contains("\\,") ? "\"" + keys.get(i) + "\"" : keys.get(i), - values.get(i).contains("\\,") ? "\"" + values.get(i) + "\"" : values.get(i) + tags.put( // be sure to surround any values containing escapes with double quotes + keys.get(i).contains("\\") ? "\"" + keys.get(i) + "\"" : keys.get(i), + values.get(i).contains("\\") ? "\"" + values.get(i) + "\"" : values.get(i) ); } } From c900bf6be7abac976af0931a39531a4e8d75d529 Mon Sep 17 00:00:00 2001 From: karel rehor Date: Wed, 18 Mar 2026 15:14:48 +0100 Subject: [PATCH 06/13] chore: fix lint issue --- .../com/influxdb/client/internal/InfluxQLQueryApiImpl.java | 3 ++- 1 file changed, 2 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 becd1e5dae..29dd02d4f7 100644 --- a/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java +++ b/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java @@ -220,7 +220,8 @@ private static Map parseTags(@Nonnull final String value) { } } for (int i = 0; i < keys.size(); i++) { - tags.put( // be sure to surround any values containing escapes with double quotes + // be sure to surround any values containing escapes with double quotes + tags.put( keys.get(i).contains("\\") ? "\"" + keys.get(i) + "\"" : keys.get(i), values.get(i).contains("\\") ? "\"" + values.get(i) + "\"" : values.get(i) ); From 10be978a712d903632c7aff6ea44d87c001efb5f Mon Sep 17 00:00:00 2001 From: karel rehor Date: Wed, 18 Mar 2026 16:50:57 +0100 Subject: [PATCH 07/13] chore: remove key and value lists from parseTags() --- .../client/internal/InfluxQLQueryApiImpl.java | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 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 29dd02d4f7..afb38fb5a1 100644 --- a/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java +++ b/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java @@ -182,7 +182,7 @@ static InfluxQLQueryResult readInfluxQLResult( private static int indexOfUnescapedChar(@Nonnull final String str, final char ch) { char[] chars = str.toCharArray(); - for (int i = 0; i < chars.length; i++) { + for (int i = 1; i < chars.length; i++) { // ignore first value if (chars[i] == ch && chars[i - 1] != '\\') { return i; } @@ -195,36 +195,35 @@ private static int indexOfUnescapedChar(@Nonnull final String str, final char ch expression. So parsing begins based on the verb ('=') not on the assumed expression termination character (','). The Left and right values of the split based on ('=') are collected and checked for the correct statement terminator (an unescaped ','). Any value on the left of an unescaped ',' is a - value. Any value on the right is a key. These are placed in their respective ordered lists and then - recombined into the tags HashMap. + value. Any value on the right is a key. */ private static Map parseTags(@Nonnull final String value) { final Map tags = new HashMap<>(); - final List keys = new ArrayList<>(); - final List values = new ArrayList<>(); if (!value.isEmpty()) { String[] chunks = value.split("="); + String currentKey = ""; + String currentValue = ""; + String nextKey = ""; for (int i = 0; i < chunks.length; i++) { if (i == 0) { // first element will be a key on its own. - keys.add(chunks[i]); + nextKey = chunks[i]; } else if (i == chunks.length - 1) { // the last element will be a value on its own. - values.add(chunks[i]); + currentValue = chunks[i]; } else { // check for legitimate keys and values int commaIndex = indexOfUnescapedChar(chunks[i], ','); if (commaIndex != -1) { - String v = chunks[i].substring(0, commaIndex); - String k = chunks[i].substring(commaIndex + 1); - keys.add(k); - values.add(v); + currentValue = chunks[i].substring(0, commaIndex); + nextKey = chunks[i].substring(commaIndex + 1); } } - } - for (int i = 0; i < keys.size(); i++) { - // be sure to surround any values containing escapes with double quotes - tags.put( - keys.get(i).contains("\\") ? "\"" + keys.get(i) + "\"" : keys.get(i), - values.get(i).contains("\\") ? "\"" + values.get(i) + "\"" : values.get(i) - ); + if (i > 0) { + // be sure to surround keys and values containing escapes with double quotes + tags.put( + currentKey.contains("\\") ? "\"" + currentKey + "\"" : currentKey, + currentValue.contains("\\") ? "\"" + currentValue + "\"" : currentValue + ); + } + currentKey = nextKey; } } From bab2453f6a88c946e0c998cf2d93eb231b0a9a87 Mon Sep 17 00:00:00 2001 From: karel rehor Date: Wed, 18 Mar 2026 17:09:40 +0100 Subject: [PATCH 08/13] tests: add assertion that all tags in series are accounted for --- .../com/influxdb/client/internal/InfluxQLQueryApiImplTest.java | 1 + 1 file changed, 1 insertion(+) 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 64bfb89ee6..daf61fd86a 100644 --- a/client/src/test/java/com/influxdb/client/internal/InfluxQLQueryApiImplTest.java +++ b/client/src/test/java/com/influxdb/client/internal/InfluxQLQueryApiImplTest.java @@ -164,6 +164,7 @@ void readInfluxQLResultWithTagCommas() throws IOException { } } } + Assertions.assertThat(index).isEqualTo(testTags.size()); } /* From 5b99621197ce97616edf3d5001d22e6d568c7c69 Mon Sep 17 00:00:00 2001 From: karel rehor Date: Thu, 19 Mar 2026 14:11:10 +0100 Subject: [PATCH 09/13] chore: rewrite InfluxQLQueryApiImpl.parseTags to handle escaped equals and backslashes. --- .../client/internal/InfluxQLQueryApiImpl.java | 108 ++++++++++++------ .../internal/InfluxQLQueryApiImplTest.java | 11 +- 2 files changed, 79 insertions(+), 40 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 afb38fb5a1..3c3a5f2960 100644 --- a/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java +++ b/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java @@ -180,50 +180,82 @@ static InfluxQLQueryResult readInfluxQLResult( return new InfluxQLQueryResult(results); } - private static int indexOfUnescapedChar(@Nonnull final String str, final char ch) { - char[] chars = str.toCharArray(); - for (int i = 1; i < chars.length; i++) { // ignore first value - if (chars[i] == ch && chars[i - 1] != '\\') { - return i; - } - } - return -1; - } - - /* - This works on the principle that the copula '=' is the _governing verb_ of any key to value - expression. So parsing begins based on the verb ('=') not on the assumed expression termination - character (','). The Left and right values of the split based on ('=') are collected and checked for - the correct statement terminator (an unescaped ','). Any value on the left of an unescaped ',' is a - value. Any value on the right is a key. - */ private static Map parseTags(@Nonnull final String value) { final Map tags = new HashMap<>(); - if (!value.isEmpty()) { - String[] chunks = value.split("="); - String currentKey = ""; - String currentValue = ""; - String nextKey = ""; - for (int i = 0; i < chunks.length; i++) { - if (i == 0) { // first element will be a key on its own. - nextKey = chunks[i]; - } else if (i == chunks.length - 1) { // the last element will be a value on its own. - currentValue = chunks[i]; - } else { // check for legitimate keys and values - int commaIndex = indexOfUnescapedChar(chunks[i], ','); - if (commaIndex != -1) { - currentValue = chunks[i].substring(0, commaIndex); - nextKey = chunks[i].substring(commaIndex + 1); - } + 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); } - if (i > 0) { - // be sure to surround keys and values containing escapes with double quotes + escaped = false; + continue; + } + + if (c == '\\') { + // start escape sequence + // preserve escape character + if (inValue) { + currentValue.append(c); + } else { + currentKey.append(c); + } + 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( - currentKey.contains("\\") ? "\"" + currentKey + "\"" : currentKey, - currentValue.contains("\\") ? "\"" + currentValue + "\"" : currentValue + key.contains("\\") ? "\"" + key + "\"" : key, + val.contains("\\") ? "\"" + val + "\"" : val ); } - currentKey = nextKey; + 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 (!key.isEmpty() || inValue) { + if (!key.isEmpty()) { + tags.put( + key.contains("\\") ? "\"" + key + "\"" : key, + val.contains("\\") ? "\"" + val + "\"" : val + ); } } 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 daf61fd86a..9af6df98bc 100644 --- a/client/src/test/java/com/influxdb/client/internal/InfluxQLQueryApiImplTest.java +++ b/client/src/test/java/com/influxdb/client/internal/InfluxQLQueryApiImplTest.java @@ -72,7 +72,7 @@ void readInfluxQLResultWithTagCommas() throws IOException { "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", // multiple commas in key and value + "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 ); @@ -125,7 +125,7 @@ void readInfluxQLResultWithTagCommas() throws IOException { // 9. multiple commas in key and value new AbstractMap.SimpleImmutableEntry<>(testTags.get(8), new HashMap() {{ - put("\"silly\\,long\\,tag\"", "\"a\\,b\\,\\ c\\,\\ d\""); + put("\"silly\\,\\=long\\,tag\"", "\"a\\,b\\\\\\,\\ c\\,\\ d\""); }}), // legacy broken tags new AbstractMap.SimpleImmutableEntry<>(testTags.get(9), @@ -161,6 +161,13 @@ void readInfluxQLResultWithTagCommas() throws IOException { 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"); } } } From b2ea11859f3a691cbd675bbeb36833159b789546 Mon Sep 17 00:00:00 2001 From: karel rehor Date: Thu, 19 Mar 2026 15:04:58 +0100 Subject: [PATCH 10/13] tests: improve assert and readability of tags test --- .../internal/InfluxQLQueryApiImplTest.java | 79 ++++++++++--------- 1 file changed, 42 insertions(+), 37 deletions(-) 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 9af6df98bc..57470f9acc 100644 --- a/client/src/test/java/com/influxdb/client/internal/InfluxQLQueryApiImplTest.java +++ b/client/src/test/java/com/influxdb/client/internal/InfluxQLQueryApiImplTest.java @@ -37,6 +37,8 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; +import javax.annotation.Nonnull; + class InfluxQLQueryApiImplTest { private static final Cancellable NO_CANCELLING = new Cancellable() { @@ -50,6 +52,17 @@ 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; + } + @Test void readInfluxQLResultWithTagCommas() throws IOException { InfluxQLQueryResult.Series.ValueExtractor extractValue = (columnName, rawValue, resultIndex, seriesName) -> { @@ -79,60 +92,52 @@ void readInfluxQLResultWithTagCommas() throws IOException { Map> expectedTagsMap = Stream.of( // 1. simpleTag new AbstractMap.SimpleImmutableEntry<>(testTags.get(0), - new HashMap() {{ - put("location", "Cheb_CZ"); - }}), + mapOf("location", "Cheb_CZ")), // 2. standardTags * 2 new AbstractMap.SimpleImmutableEntry<>(testTags.get(1), - new HashMap() {{ - put("region", "us-east-1"); - put("host", "server1"); - }}), + mapOf( + "region", "us-east-1", + "host", "server1" + )), // 3. simpleTag with value comma and space new AbstractMap.SimpleImmutableEntry<>(testTags.get(2), - new HashMap() {{ - put("location", "\"Cheb\\,\\ CZ\""); - }}), + mapOf("location", "\"Cheb\\,\\ CZ\"")), // 4. multiple tags with underscore new AbstractMap.SimpleImmutableEntry<>(testTags.get(3), - new HashMap() {{ - put("location", "Cheb_CZ"); - put("branch", "Munchen_DE"); - }}), + mapOf( + "location", "Cheb_CZ", + "branch", "Munchen_DE" + )), // 5. multiple tags with comma and space new AbstractMap.SimpleImmutableEntry<>(testTags.get(4), - new HashMap() {{ - put("location", "\"Cheb\\,\\ CZ\""); - put("branch", "\"Munchen\\,\\ DE\""); - }}), + mapOf( + "location", "\"Cheb\\,\\ CZ\"", + "branch", "\"Munchen\\,\\ DE\"" + )), // 6. tag with comma and space in key new AbstractMap.SimpleImmutableEntry<>(testTags.get(5), - new HashMap() {{ - put("\"model\\,\\ uin\"", "C3PO"); - }}), + mapOf("\"model\\,\\ uin\"", "C3PO")), // 7. tag with comma and space in key and value new AbstractMap.SimpleImmutableEntry<>(testTags.get(6), - new HashMap() {{ - put("\"model\\,\\ uin\"", "\"Droid\\, C3PO\""); - }}), + mapOf("\"model\\,\\ uin\"", "\"Droid\\, C3PO\"")), // 8. comma space in key and val with multiple tags new AbstractMap.SimpleImmutableEntry<>(testTags.get(7), - new HashMap() {{ - put("\"model\\,\\ uin\"", "\"Droid\\,\\ C3PO\""); - put("location", "\"Cheb\\,\\ CZ\""); - put("branch", "\"Munchen\\,\\ DE\""); - }}), + mapOf( + "\"model\\,\\ uin\"", "\"Droid\\,\\ C3PO\"", + "location", "\"Cheb\\,\\ CZ\"", + "branch", "\"Munchen\\,\\ DE\"" + )), // 9. multiple commas in key and value new AbstractMap.SimpleImmutableEntry<>(testTags.get(8), - new HashMap() {{ - put("\"silly\\,\\=long\\,tag\"", "\"a\\,b\\\\\\,\\ c\\,\\ d\""); - }}), + mapOf( + "\"silly\\,\\=long\\,tag\"", "\"a\\,b\\\\\\,\\ c\\,\\ d\"" + )), // legacy broken tags new AbstractMap.SimpleImmutableEntry<>(testTags.get(9), - new HashMap() {{ - put("region", "\"us\\,\\ east-1\""); - put("\"host\\,\\ name\"", "\"ser\\,\\ ver1\""); - }}) + 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" @@ -156,7 +161,7 @@ void readInfluxQLResultWithTagCommas() throws IOException { for(InfluxQLQueryResult.Result r : results) { for(InfluxQLQueryResult.Series s : r.getSeries()){ Assertions.assertThat(s.getTags()).isEqualTo(expectedTagsMap.get(testTags.get(index++))); - if(index < 9) { + 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")); From 219a792ee9dde1af26b13fa7e52b1f634b291ee7 Mon Sep 17 00:00:00 2001 From: karel rehor Date: Thu, 19 Mar 2026 16:25:45 +0100 Subject: [PATCH 11/13] chore: remove needless encapsulation of escaped strings in double quotes. --- .../client/internal/InfluxQLQueryApiImpl.java | 10 ++------ .../internal/InfluxQLQueryApiImplTest.java | 25 +++++++++++-------- 2 files changed, 16 insertions(+), 19 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 3c3a5f2960..6e3302cb65 100644 --- a/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java +++ b/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java @@ -229,10 +229,7 @@ private static Map parseTags(@Nonnull final String value) { String key = currentKey.toString(); String val = currentValue.toString(); if (!key.isEmpty()) { - tags.put( - key.contains("\\") ? "\"" + key + "\"" : key, - val.contains("\\") ? "\"" + val + "\"" : val - ); + tags.put(key, val); } currentKey.setLength(0); currentValue.setLength(0); @@ -252,10 +249,7 @@ private static Map parseTags(@Nonnull final String value) { String val = currentValue.toString(); if (!key.isEmpty() || inValue) { if (!key.isEmpty()) { - tags.put( - key.contains("\\") ? "\"" + key + "\"" : key, - val.contains("\\") ? "\"" + val + "\"" : val - ); + tags.put(key, val); } } 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 57470f9acc..da1348222f 100644 --- a/client/src/test/java/com/influxdb/client/internal/InfluxQLQueryApiImplTest.java +++ b/client/src/test/java/com/influxdb/client/internal/InfluxQLQueryApiImplTest.java @@ -34,6 +34,9 @@ import com.influxdb.Cancellable; import com.influxdb.query.InfluxQLQueryResult; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVParser; +import org.apache.commons.csv.CSVRecord; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; @@ -101,7 +104,7 @@ void readInfluxQLResultWithTagCommas() throws IOException { )), // 3. simpleTag with value comma and space new AbstractMap.SimpleImmutableEntry<>(testTags.get(2), - mapOf("location", "\"Cheb\\,\\ CZ\"")), + mapOf("location", "Cheb\\,\\ CZ")), // 4. multiple tags with underscore new AbstractMap.SimpleImmutableEntry<>(testTags.get(3), mapOf( @@ -111,32 +114,32 @@ void readInfluxQLResultWithTagCommas() throws IOException { // 5. multiple tags with comma and space new AbstractMap.SimpleImmutableEntry<>(testTags.get(4), mapOf( - "location", "\"Cheb\\,\\ CZ\"", - "branch", "\"Munchen\\,\\ DE\"" + "location", "Cheb\\,\\ CZ", + "branch", "Munchen\\,\\ DE" )), // 6. tag with comma and space in key new AbstractMap.SimpleImmutableEntry<>(testTags.get(5), - mapOf("\"model\\,\\ uin\"", "C3PO")), + 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\"")), + 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\"" + "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\"" + "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\"" + "region", "us\\,\\ east-1", + "host\\,\\ name", "ser\\,\\ ver1" )) ).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); From b4059ba8b619febdac0811c1585f0ea3b51865cb Mon Sep 17 00:00:00 2001 From: karel rehor Date: Thu, 19 Mar 2026 17:05:47 +0100 Subject: [PATCH 12/13] chore: fix final tag parser logic and clean unused imports in test --- .../com/influxdb/client/internal/InfluxQLQueryApiImpl.java | 6 ++---- .../influxdb/client/internal/InfluxQLQueryApiImplTest.java | 3 --- 2 files changed, 2 insertions(+), 7 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 6e3302cb65..25bf84da00 100644 --- a/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java +++ b/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java @@ -247,10 +247,8 @@ private static Map parseTags(@Nonnull final String value) { // finalize last key/value pair if any String key = currentKey.toString(); String val = currentValue.toString(); - if (!key.isEmpty() || inValue) { - if (!key.isEmpty()) { - tags.put(key, val); - } + if (inValue && !key.isEmpty()) { + tags.put(key, val); } return tags; 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 da1348222f..ca587ab7c0 100644 --- a/client/src/test/java/com/influxdb/client/internal/InfluxQLQueryApiImplTest.java +++ b/client/src/test/java/com/influxdb/client/internal/InfluxQLQueryApiImplTest.java @@ -34,9 +34,6 @@ import com.influxdb.Cancellable; import com.influxdb.query.InfluxQLQueryResult; -import org.apache.commons.csv.CSVFormat; -import org.apache.commons.csv.CSVParser; -import org.apache.commons.csv.CSVRecord; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; From 7fe6761be29c5db525321297618c64a85f4a8ddf Mon Sep 17 00:00:00 2001 From: karel rehor Date: Fri, 20 Mar 2026 16:30:03 +0100 Subject: [PATCH 13/13] fix: adjust tag parser for escaped escapes and add integration test. --- .../client/internal/InfluxQLQueryApiImpl.java | 13 ++++++++- .../influxdb/client/ITInfluxQLQueryApi.java | 28 +++++++++++++++++++ .../internal/InfluxQLQueryApiImplTest.java | 15 +++++----- 3 files changed, 48 insertions(+), 8 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 25bf84da00..1206bc3657 100644 --- a/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java +++ b/client/src/main/java/com/influxdb/client/internal/InfluxQLQueryApiImpl.java @@ -190,6 +190,7 @@ private static Map parseTags(@Nonnull final String value) { StringBuilder currentValue = new StringBuilder(); boolean inValue = false; boolean escaped = false; + boolean firstEscaped = false; for (int i = 0; i < value.length(); i++) { char c = value.charAt(i); @@ -208,12 +209,22 @@ private static Map parseTags(@Nonnull final String value) { if (c == '\\') { // start escape sequence // preserve escape character + if (firstEscaped) { + escaped = true; + firstEscaped = false; + continue; + } if (inValue) { currentValue.append(c); } else { currentKey.append(c); } - escaped = true; + firstEscaped = true; + continue; + } + + if(firstEscaped) { + firstEscaped = false; continue; } diff --git a/client/src/test/java/com/influxdb/client/ITInfluxQLQueryApi.java b/client/src/test/java/com/influxdb/client/ITInfluxQLQueryApi.java index 45dcda1a58..84c51a6fb5 100644 --- a/client/src/test/java/com/influxdb/client/ITInfluxQLQueryApi.java +++ b/client/src/test/java/com/influxdb/client/ITInfluxQLQueryApi.java @@ -24,6 +24,8 @@ import java.io.IOException; import java.math.BigDecimal; import java.time.Instant; +import java.util.HashMap; +import java.util.Map; import com.influxdb.client.domain.Bucket; import com.influxdb.client.domain.DBRPCreate; @@ -94,6 +96,32 @@ 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 ca587ab7c0..507421cdb7 100644 --- a/client/src/test/java/com/influxdb/client/internal/InfluxQLQueryApiImplTest.java +++ b/client/src/test/java/com/influxdb/client/internal/InfluxQLQueryApiImplTest.java @@ -76,17 +76,18 @@ void readInfluxQLResultWithTagCommas() throws IOException { 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", // 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 + "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(