diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a862ef..2e1e805 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,14 @@ -## 0.10.0 [unreleased] +## 1.0.0 [unreleased] + +### Features + +1. [#200](https://github.com/InfluxCommunity/influxdb3-java/pull/200): Respect iox::column_type::field metadata when + mapping query results into values. + - iox::column_type::field::integer: => Long + - iox::column_type::field::uinteger: => Long + - iox::column_type::field::float: => Double + - iox::column_type::field::string: => String + - iox::column_type::field::boolean: => Boolean ### Dependencies diff --git a/pom.xml b/pom.xml index 4909797..383856c 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,7 @@ com.influxdb influxdb3-java jar - 0.10.0-SNAPSHOT + 1.0.0-SNAPSHOT InfluxDB 3 Java Client 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 f43178b..6d67b7d 100644 --- a/src/main/java/com/influxdb/v3/client/internal/InfluxDBClientImpl.java +++ b/src/main/java/com/influxdb/v3/client/internal/InfluxDBClientImpl.java @@ -24,7 +24,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -181,19 +180,13 @@ public Stream query(@Nonnull final String query, @Nonnull final Map parameters, @Nonnull final QueryOptions options) { return queryData(query, parameters, options) - .flatMap(vector -> { - List fieldVectors = vector.getFieldVectors(); - return IntStream - .range(0, vector.getRowCount()) - .mapToObj(rowNumber -> { - - ArrayList row = new ArrayList<>(); - for (FieldVector fieldVector : fieldVectors) { - row.add(fieldVector.getObject(rowNumber)); - } - return row.toArray(); - }); - }); + .flatMap(vector -> IntStream.range(0, vector.getRowCount()) + .mapToObj(rowNumber -> + VectorSchemaRootConverter.INSTANCE + .getArrayObjectFromVectorSchemaRoot( + vector, + rowNumber + ))); } @Nonnull @@ -225,7 +218,7 @@ public Stream queryPoints(@Nonnull final String query, return IntStream .range(0, vector.getRowCount()) .mapToObj(row -> - VectorSchemaRootConverter.INSTANCE.toPointValues(row, vector, fieldVectors)); + VectorSchemaRootConverter.INSTANCE.toPointValues(row, fieldVectors)); }); } diff --git a/src/main/java/com/influxdb/v3/client/internal/NanosecondConverter.java b/src/main/java/com/influxdb/v3/client/internal/NanosecondConverter.java index f47c922..102f481 100644 --- a/src/main/java/com/influxdb/v3/client/internal/NanosecondConverter.java +++ b/src/main/java/com/influxdb/v3/client/internal/NanosecondConverter.java @@ -24,11 +24,18 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.function.Function; +import javax.annotation.Nonnull; import javax.annotation.Nullable; +import org.apache.arrow.vector.types.pojo.ArrowType; +import org.apache.arrow.vector.types.pojo.Field; + import com.influxdb.v3.client.write.WritePrecision; import static java.util.function.Function.identity; @@ -111,4 +118,55 @@ public static BigInteger convert(final Instant instant, final WritePrecision pre return FROM_NANOS.get(precision).apply(nanos); } + + /** + * Convert Long or LocalDateTime to timestamp nanosecond. + * + * @param value the time in Long or LocalDateTime + * @param field the arrow field metadata + * @return the time in nanosecond + */ + @Nullable + public static BigInteger getTimestampNano(@Nonnull final Object value, @Nonnull final Field field) { + BigInteger result = null; + + if (value instanceof Long) { + if (field.getFieldType().getType() instanceof ArrowType.Timestamp) { + ArrowType.Timestamp type = (ArrowType.Timestamp) field.getFieldType().getType(); + TimeUnit timeUnit; + switch (type.getUnit()) { + case SECOND: + timeUnit = TimeUnit.SECONDS; + break; + case MILLISECOND: + timeUnit = TimeUnit.MILLISECONDS; + break; + case MICROSECOND: + timeUnit = TimeUnit.MICROSECONDS; + break; + case NANOSECOND: + default: + timeUnit = TimeUnit.NANOSECONDS; + break; + } + long nanoseconds = TimeUnit.NANOSECONDS.convert((Long) value, timeUnit); + Instant instant = Instant.ofEpochSecond(0, nanoseconds); + result = convertInstantToNano(instant); + } else { + Instant instant = Instant.ofEpochMilli((Long) value); + result = convertInstantToNano(instant); + } + } else if (value instanceof LocalDateTime) { + Instant instant = ((LocalDateTime) value).toInstant(ZoneOffset.UTC); + result = convertInstantToNano(instant); + } + return result; + } + + @Nullable + private static BigInteger convertInstantToNano(@Nonnull final Instant instant) { + var writePrecision = WritePrecision.NS; + BigInteger convertedTime = NanosecondConverter.convert(instant, writePrecision); + return NanosecondConverter.convertToNanos(convertedTime, writePrecision); + } } diff --git a/src/main/java/com/influxdb/v3/client/internal/TypeCasting.java b/src/main/java/com/influxdb/v3/client/internal/TypeCasting.java new file mode 100644 index 0000000..981a381 --- /dev/null +++ b/src/main/java/com/influxdb/v3/client/internal/TypeCasting.java @@ -0,0 +1,81 @@ +/* + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.influxdb.v3.client.internal; + +import javax.annotation.Nonnull; + +import org.apache.arrow.vector.util.Text; + +/** + * Functions for safe type casting. + */ +public final class TypeCasting { + + private TypeCasting() { } + + /** + * Safe casting to long value. + * + * @param value object to cast + * @return long value + */ + public static long toLongValue(@Nonnull final Object value) { + + if (long.class.isAssignableFrom(value.getClass()) + || Long.class.isAssignableFrom(value.getClass())) { + return (long) value; + } + + return ((Number) value).longValue(); + } + + /** + * Safe casting to double value. + * + * @param value object to cast + * @return double value + */ + public static double toDoubleValue(@Nonnull final Object value) { + + if (double.class.isAssignableFrom(value.getClass()) + || Double.class.isAssignableFrom(value.getClass())) { + return (double) value; + } + + return ((Number) value).doubleValue(); + } + + /** + * Safe casting to string value. + * + * @param value object to cast + * @return string value + */ + public static String toStringValue(@Nonnull final Object value) { + + if (Text.class.isAssignableFrom(value.getClass())) { + return value.toString(); + } + + return (String) value; + } +} diff --git a/src/main/java/com/influxdb/v3/client/internal/VectorSchemaRootConverter.java b/src/main/java/com/influxdb/v3/client/internal/VectorSchemaRootConverter.java index d958705..b2ce958 100644 --- a/src/main/java/com/influxdb/v3/client/internal/VectorSchemaRootConverter.java +++ b/src/main/java/com/influxdb/v3/client/internal/VectorSchemaRootConverter.java @@ -21,22 +21,22 @@ */ package com.influxdb.v3.client.internal; -import java.time.Instant; +import java.math.BigInteger; import java.time.LocalDateTime; -import java.time.ZoneOffset; import java.util.List; import java.util.Objects; -import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; import org.apache.arrow.vector.FieldVector; import org.apache.arrow.vector.VectorSchemaRoot; -import org.apache.arrow.vector.types.pojo.ArrowType; import org.apache.arrow.vector.types.pojo.Field; import org.apache.arrow.vector.util.Text; import com.influxdb.v3.client.PointValues; +import com.influxdb.v3.client.write.WritePrecision; /** @@ -46,94 +46,153 @@ * This class is thread-safe. */ @ThreadSafe -final class VectorSchemaRootConverter { +public final class VectorSchemaRootConverter { - static final VectorSchemaRootConverter INSTANCE = new VectorSchemaRootConverter(); + private static final Logger LOG = Logger.getLogger(VectorSchemaRootConverter.class.getName()); + + public static final VectorSchemaRootConverter INSTANCE = new VectorSchemaRootConverter(); /** * Converts a given row of data from a VectorSchemaRoot object to PointValues. * * @param rowNumber the index of the row to be converted - * @param vector the VectorSchemaRoot object containing the data * @param fieldVectors the list of FieldVector objects representing the data columns * @return the converted PointValues object */ @Nonnull PointValues toPointValues(final int rowNumber, - @Nonnull final VectorSchemaRoot vector, @Nonnull final List fieldVectors) { PointValues p = new PointValues(); - for (int i = 0; i < fieldVectors.size(); i++) { - var schema = vector.getSchema().getFields().get(i); - var value = fieldVectors.get(i).getObject(rowNumber); - var name = schema.getName(); - var metaType = schema.getMetadata().get("iox::column::type"); + for (FieldVector fieldVector : fieldVectors) { + var field = fieldVector.getField(); + var value = fieldVector.getObject(rowNumber); + var fieldName = field.getName(); + var metaType = field.getMetadata().get("iox::column::type"); if (value instanceof Text) { value = value.toString(); } - if ((Objects.equals(name, "measurement") - || Objects.equals(name, "iox::measurement")) + if ((Objects.equals(fieldName, "measurement") + || Objects.equals(fieldName, "iox::measurement")) && value instanceof String) { p.setMeasurement((String) value); continue; } if (metaType == null) { - if (Objects.equals(name, "time") && (value instanceof Long || value instanceof LocalDateTime)) { - setTimestamp(value, schema, p); + if (Objects.equals(fieldName, "time") && (value instanceof Long || value instanceof LocalDateTime)) { + var timeNano = NanosecondConverter.getTimestampNano(value, field); + p.setTimestamp(timeNano, WritePrecision.NS); } else { // just push as field If you don't know what type is it - p.setField(name, value); + p.setField(fieldName, value); } continue; } - String[] parts = metaType.split("::"); - String valueType = parts[2]; - + String valueType = metaType.split("::")[2]; + Object mappedValue = getMappedValue(field, value); if ("field".equals(valueType)) { - p.setField(name, value); + p.setField(fieldName, mappedValue); } else if ("tag".equals(valueType) && value instanceof String) { - p.setTag(name, (String) value); + p.setTag(fieldName, (String) mappedValue); } else if ("timestamp".equals(valueType)) { - setTimestamp(value, schema, p); + p.setTimestamp((BigInteger) mappedValue, WritePrecision.NS); } } return p; } - private void setTimestamp(@Nonnull final Object value, - @Nonnull final Field schema, - @Nonnull final PointValues pointValues) { - if (value instanceof Long) { - if (schema.getFieldType().getType() instanceof ArrowType.Timestamp) { - ArrowType.Timestamp type = (ArrowType.Timestamp) schema.getFieldType().getType(); - TimeUnit timeUnit; - switch (type.getUnit()) { - case SECOND: - timeUnit = TimeUnit.SECONDS; - break; - case MILLISECOND: - timeUnit = TimeUnit.MILLISECONDS; - break; - case MICROSECOND: - timeUnit = TimeUnit.MICROSECONDS; - break; - default: - case NANOSECOND: - timeUnit = TimeUnit.NANOSECONDS; - break; - } - long nanoseconds = TimeUnit.NANOSECONDS.convert((Long) value, timeUnit); - pointValues.setTimestamp(Instant.ofEpochSecond(0, nanoseconds)); + /** + * Function to cast value return base on metadata from InfluxDB. + * + * @param field the Field object from Arrow + * @param value the value to cast + * @return the value with the correct type + */ + public Object getMappedValue(@Nonnull final Field field, @Nullable final Object value) { + if (value == null) { + return null; + } + + var fieldName = field.getName(); + if ("measurement".equals(fieldName) || "iox::measurement".equals(fieldName)) { + return value.toString(); + } + + var metaType = field.getMetadata().get("iox::column::type"); + if (metaType == null) { + if ("time".equals(fieldName) && (value instanceof Long || value instanceof LocalDateTime)) { + return NanosecondConverter.getTimestampNano(value, field); } else { - pointValues.setTimestamp(Instant.ofEpochMilli((Long) value)); + return value; } - } else if (value instanceof LocalDateTime) { - pointValues.setTimestamp(((LocalDateTime) value).toInstant(ZoneOffset.UTC)); } + + String[] parts = metaType.split("::"); + String valueType = parts[2]; + if ("field".equals(valueType)) { + switch (metaType) { + case "iox::column_type::field::integer": + case "iox::column_type::field::uinteger": + if (value instanceof Number) { + return TypeCasting.toLongValue(value); + } else { + LOG.warning(String.format("Value %s is not an Long", value)); + return value; + } + case "iox::column_type::field::float": + if (value instanceof Number) { + return TypeCasting.toDoubleValue(value); + } else { + LOG.warning(String.format("Value %s is not a Double", value)); + return value; + } + case "iox::column_type::field::string": + if (value instanceof Text || value instanceof String) { + return TypeCasting.toStringValue(value); + } else { + LOG.warning(String.format("Value %s is not a String", value)); + return value; + } + case "iox::column_type::field::boolean": + if (value instanceof Boolean) { + return value; + } else { + LOG.warning(String.format("Value %s is not a Boolean", value)); + return value; + } + default: + return value; + } + } else if ("timestamp".equals(valueType) || Objects.equals(fieldName, "time")) { + return NanosecondConverter.getTimestampNano(value, field); + } else { + return TypeCasting.toStringValue(value); + } + } + + /** + * Get array of values from VectorSchemaRoot. + * + * @param vector The data return from InfluxDB. + * @param rowNumber The row number of data + * @return An array of Objects represents a row of data + */ + public Object[] getArrayObjectFromVectorSchemaRoot(@Nonnull final VectorSchemaRoot vector, final int rowNumber) { + List fieldVectors = vector.getFieldVectors(); + int columnSize = fieldVectors.size(); + var row = new Object[columnSize]; + for (int i = 0; i < columnSize; i++) { + FieldVector fieldVector = fieldVectors.get(i); + row[i] = getMappedValue( + fieldVector.getField(), + fieldVector.getObject(rowNumber) + ); + } + + return row; } } diff --git a/src/test/java/com/influxdb/v3/client/InfluxDBClientTest.java b/src/test/java/com/influxdb/v3/client/InfluxDBClientTest.java index 2c9fdde..f1e868a 100644 --- a/src/test/java/com/influxdb/v3/client/InfluxDBClientTest.java +++ b/src/test/java/com/influxdb/v3/client/InfluxDBClientTest.java @@ -21,13 +21,21 @@ */ package com.influxdb.v3.client; +import java.math.BigInteger; +import java.time.Instant; import java.util.Map; import java.util.Properties; +import java.util.UUID; +import java.util.stream.Stream; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -class InfluxDBClientTest { +import com.influxdb.v3.client.write.WriteOptions; +import com.influxdb.v3.client.write.WritePrecision; + +public class InfluxDBClientTest { @Test void requiredHost() { @@ -116,4 +124,53 @@ public void unsupportedQueryParams() throws Exception { + "class com.influxdb.v3.client.internal.InfluxDBClientImpl"); } } + + @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_URL", matches = ".*") + @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_TOKEN", matches = ".*") + @EnabledIfEnvironmentVariable(named = "TESTING_INFLUXDB_DATABASE", matches = ".*") + @Test + public void testQuery() throws Exception { + try (InfluxDBClient client = InfluxDBClient.getInstance( + System.getenv("TESTING_INFLUXDB_URL"), + System.getenv("TESTING_INFLUXDB_TOKEN").toCharArray(), + System.getenv("TESTING_INFLUXDB_DATABASE"), + null)) { + String uuid = UUID.randomUUID().toString(); + long timestamp = Instant.now().getEpochSecond(); + String record = String.format( + "host10,tag=empty " + + "name=\"intel\"," + + "mem_total=2048," + + "disk_free=100i," + + "temperature=100.86," + + "isActive=true," + + "testId=\"%s\" %d", + uuid, + timestamp + ); + client.writeRecord(record, new WriteOptions(null, WritePrecision.S, null)); + + Map parameters = Map.of("testId", uuid); + String sql = "Select * from host10 where \"testId\"=$testId"; + try (Stream stream = client.query(sql, parameters)) { + stream.findFirst() + .ifPresent(objects -> { + Assertions.assertThat(objects[0].getClass()).isEqualTo(Long.class); + Assertions.assertThat(objects[0]).isEqualTo(100L); + + Assertions.assertThat(objects[1].getClass()).isEqualTo(Boolean.class); + Assertions.assertThat(objects[1]).isEqualTo(true); + + Assertions.assertThat(objects[2].getClass()).isEqualTo(Double.class); + Assertions.assertThat(objects[2]).isEqualTo(2048.0); + + Assertions.assertThat(objects[3].getClass()).isEqualTo(String.class); + Assertions.assertThat(objects[3]).isEqualTo("intel"); + + Assertions.assertThat(objects[7].getClass()).isEqualTo(BigInteger.class); + Assertions.assertThat(objects[7]).isEqualTo(BigInteger.valueOf(timestamp * 1_000_000_000)); + }); + } + } + } } diff --git a/src/test/java/com/influxdb/v3/client/internal/NanosecondConverterTest.java b/src/test/java/com/influxdb/v3/client/internal/NanosecondConverterTest.java new file mode 100644 index 0000000..31ea7f1 --- /dev/null +++ b/src/test/java/com/influxdb/v3/client/internal/NanosecondConverterTest.java @@ -0,0 +1,91 @@ +/* + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.influxdb.v3.client.internal; + +import java.math.BigInteger; + +import org.apache.arrow.vector.types.TimeUnit; +import org.apache.arrow.vector.types.pojo.ArrowType; +import org.apache.arrow.vector.types.pojo.Field; +import org.apache.arrow.vector.types.pojo.FieldType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class NanosecondConverterTest { + + @Test + void testGetTimestampNanosecond() { + BigInteger timestampNanoSecond = null; + + // Second + FieldType timeTypeSecond = new FieldType(true, + new ArrowType.Timestamp(TimeUnit.SECOND, "UTC"), + null); + Field timeFieldSecond = new Field("time", timeTypeSecond, null); + timestampNanoSecond = NanosecondConverter.getTimestampNano(123_456L, timeFieldSecond); + Assertions.assertEquals( + BigInteger.valueOf(123_456L) + .multiply(BigInteger.valueOf(1_000_000_000)), timestampNanoSecond + ); + + // MilliSecond + FieldType timeTypeMilliSecond = new FieldType(true, + new ArrowType.Timestamp(TimeUnit.MILLISECOND, "UTC"), + null); + Field timeFieldMilliSecond = new Field("time", timeTypeMilliSecond, null); + timestampNanoSecond = NanosecondConverter.getTimestampNano(123_456L, timeFieldMilliSecond); + Assertions.assertEquals( + BigInteger.valueOf(123_456L) + .multiply(BigInteger.valueOf(1_000_000)), timestampNanoSecond + ); + + // MicroSecond + FieldType timeTypeMicroSecond = new FieldType(true, + new ArrowType.Timestamp(TimeUnit.MICROSECOND, "UTC"), + null); + Field timeFieldMicroSecond = new Field("time", timeTypeMicroSecond, null); + timestampNanoSecond = NanosecondConverter.getTimestampNano(123_456L, timeFieldMicroSecond); + Assertions.assertEquals( + BigInteger.valueOf(123_456L) + .multiply(BigInteger.valueOf(1_000)), timestampNanoSecond + ); + + // Nano Second + FieldType timeTypeNanoSecond = new FieldType(true, + new ArrowType.Timestamp(TimeUnit.NANOSECOND, "UTC"), + null); + Field timeFieldNanoSecond = new Field("time", timeTypeNanoSecond, null); + timestampNanoSecond = NanosecondConverter.getTimestampNano(123_456L, timeFieldNanoSecond); + Assertions.assertEquals(BigInteger.valueOf(123_456L), timestampNanoSecond); + + // For ArrowType.Time type + FieldType timeMilliSecond = new FieldType(true, + new ArrowType.Time(TimeUnit.MILLISECOND, 32), + null); + Field fieldMilliSecond = new Field("time", timeMilliSecond, null); + timestampNanoSecond = NanosecondConverter.getTimestampNano(123_456L, fieldMilliSecond); + Assertions.assertEquals( + BigInteger.valueOf(123_456L) + .multiply(BigInteger.valueOf(1_000_000)), timestampNanoSecond + ); + } +} diff --git a/src/test/java/com/influxdb/v3/client/internal/TypeCastingTest.java b/src/test/java/com/influxdb/v3/client/internal/TypeCastingTest.java new file mode 100644 index 0000000..5b77fe6 --- /dev/null +++ b/src/test/java/com/influxdb/v3/client/internal/TypeCastingTest.java @@ -0,0 +1,57 @@ +/* + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.influxdb.v3.client.internal; + +import org.apache.arrow.vector.util.Text; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class TypeCastingTest { + + @Test + void testToLongValue() { + Assertions.assertEquals(1, TypeCasting.toLongValue(1)); + Assertions.assertEquals(1.0, TypeCasting.toLongValue(1.23)); + + Assertions.assertThrows(ClassCastException.class, + () -> TypeCasting.toLongValue("1")); + } + + @Test + void testToDoubleValue() { + Assertions.assertEquals(1.23, TypeCasting.toDoubleValue(1.23)); + Assertions.assertEquals(1.0, TypeCasting.toDoubleValue(1)); + + Assertions.assertThrows(ClassCastException.class, + () -> TypeCasting.toDoubleValue("1.2")); + } + + @Test + void testToStringValue() { + Assertions.assertEquals("test", TypeCasting.toStringValue("test")); + Assertions.assertEquals("test", + TypeCasting.toStringValue(new Text("test"))); + + Assertions.assertThrows(ClassCastException.class, + () -> TypeCasting.toStringValue(1)); + } +} diff --git a/src/test/java/com/influxdb/v3/client/internal/VectorSchemaRootConverterTest.java b/src/test/java/com/influxdb/v3/client/internal/VectorSchemaRootConverterTest.java index 7915b2b..dbbd69a 100644 --- a/src/test/java/com/influxdb/v3/client/internal/VectorSchemaRootConverterTest.java +++ b/src/test/java/com/influxdb/v3/client/internal/VectorSchemaRootConverterTest.java @@ -23,12 +23,10 @@ import java.math.BigInteger; import java.nio.charset.StandardCharsets; -import java.util.Arrays; import java.util.HashMap; import java.util.Map; import javax.annotation.Nonnull; -import org.apache.arrow.memory.RootAllocator; import org.apache.arrow.vector.BaseFixedWidthVector; import org.apache.arrow.vector.BigIntVector; import org.apache.arrow.vector.TimeMilliVector; @@ -39,9 +37,7 @@ import org.apache.arrow.vector.types.pojo.ArrowType; import org.apache.arrow.vector.types.pojo.Field; import org.apache.arrow.vector.types.pojo.FieldType; -import org.apache.arrow.vector.types.pojo.Schema; import org.assertj.core.api.Assertions; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import com.influxdb.v3.client.PointValues; @@ -52,7 +48,7 @@ class VectorSchemaRootConverterTest { void timestampAsArrowTime() { try (VectorSchemaRoot root = createTimeVector(1234, new ArrowType.Time(TimeUnit.MILLISECOND, 32))) { - PointValues pointValues = VectorSchemaRootConverter.INSTANCE.toPointValues(0, root, root.getFieldVectors()); + PointValues pointValues = VectorSchemaRootConverter.INSTANCE.toPointValues(0, root.getFieldVectors()); BigInteger expected = BigInteger.valueOf(1_234 * 1_000_000); Assertions.assertThat((BigInteger) pointValues.getTimestamp()).isEqualByComparingTo(expected); @@ -63,7 +59,7 @@ void timestampAsArrowTime() { void timestampAsArrowTimestampSecond() { try (VectorSchemaRoot root = createTimeVector(45_678, new ArrowType.Timestamp(TimeUnit.SECOND, "UTC"))) { - PointValues pointValues = VectorSchemaRootConverter.INSTANCE.toPointValues(0, root, root.getFieldVectors()); + PointValues pointValues = VectorSchemaRootConverter.INSTANCE.toPointValues(0, root.getFieldVectors()); BigInteger expected = BigInteger.valueOf(45_678L * 1_000_000_000); Assertions.assertThat((BigInteger) pointValues.getTimestamp()).isEqualByComparingTo(expected); @@ -74,7 +70,7 @@ void timestampAsArrowTimestampSecond() { void timestampAsArrowTimestampMillisecond() { try (VectorSchemaRoot root = createTimeVector(45_678, new ArrowType.Timestamp(TimeUnit.MILLISECOND, "UTC"))) { - PointValues pointValues = VectorSchemaRootConverter.INSTANCE.toPointValues(0, root, root.getFieldVectors()); + PointValues pointValues = VectorSchemaRootConverter.INSTANCE.toPointValues(0, root.getFieldVectors()); BigInteger expected = BigInteger.valueOf(45_678L * 1_000_000); Assertions.assertThat((BigInteger) pointValues.getTimestamp()).isEqualByComparingTo(expected); @@ -85,7 +81,7 @@ void timestampAsArrowTimestampMillisecond() { void timestampAsArrowTimestampMicrosecond() { try (VectorSchemaRoot root = createTimeVector(45_678, new ArrowType.Timestamp(TimeUnit.MICROSECOND, "UTC"))) { - PointValues pointValues = VectorSchemaRootConverter.INSTANCE.toPointValues(0, root, root.getFieldVectors()); + PointValues pointValues = VectorSchemaRootConverter.INSTANCE.toPointValues(0, root.getFieldVectors()); BigInteger expected = BigInteger.valueOf(45_678L * 1_000); Assertions.assertThat((BigInteger) pointValues.getTimestamp()).isEqualByComparingTo(expected); @@ -96,7 +92,7 @@ void timestampAsArrowTimestampMicrosecond() { void timestampAsArrowTimestampNanosecond() { try (VectorSchemaRoot root = createTimeVector(45_678, new ArrowType.Timestamp(TimeUnit.NANOSECOND, "UTC"))) { - PointValues pointValues = VectorSchemaRootConverter.INSTANCE.toPointValues(0, root, root.getFieldVectors()); + PointValues pointValues = VectorSchemaRootConverter.INSTANCE.toPointValues(0, root.getFieldVectors()); BigInteger expected = BigInteger.valueOf(45_678L); Assertions.assertThat((BigInteger) pointValues.getTimestamp()).isEqualByComparingTo(expected); @@ -107,7 +103,7 @@ void timestampAsArrowTimestampNanosecond() { void timestampAsArrowTimestampNanosecondWithoutTimezone() { try (VectorSchemaRoot root = createTimeVector(45_978, new ArrowType.Timestamp(TimeUnit.NANOSECOND, null))) { - PointValues pointValues = VectorSchemaRootConverter.INSTANCE.toPointValues(0, root, root.getFieldVectors()); + PointValues pointValues = VectorSchemaRootConverter.INSTANCE.toPointValues(0, root.getFieldVectors()); BigInteger expected = BigInteger.valueOf(45_978L); Assertions.assertThat((BigInteger) pointValues.getTimestamp()).isEqualByComparingTo(expected); @@ -118,13 +114,119 @@ void timestampAsArrowTimestampNanosecondWithoutTimezone() { void timestampAsArrowInt() { try (VectorSchemaRoot root = createTimeVector(45_678, new ArrowType.Int(64, true))) { - PointValues pointValues = VectorSchemaRootConverter.INSTANCE.toPointValues(0, root, root.getFieldVectors()); + PointValues pointValues = VectorSchemaRootConverter.INSTANCE.toPointValues(0, root.getFieldVectors()); BigInteger expected = BigInteger.valueOf(45_678L * 1_000_000); Assertions.assertThat((BigInteger) pointValues.getTimestamp()).isEqualByComparingTo(expected); } } + @Test + void getMappedValueValidMetaDataInteger() { + Field field = VectorSchemaRootUtils.generateIntField("test"); + Long value = 1L; + Object mappedValue = VectorSchemaRootConverter.INSTANCE.getMappedValue(field, value); + Assertions.assertThat(mappedValue).isEqualTo(value); + Assertions.assertThat(mappedValue.getClass()).isEqualTo(Long.class); + } + + @Test + void getMappedValueInvalidMetaDataInteger() { + Field field = VectorSchemaRootUtils.generateInvalidIntField("test"); + String value = "1"; + Object mappedValue = VectorSchemaRootConverter.INSTANCE.getMappedValue(field, value); + Assertions.assertThat(mappedValue).isEqualTo(value); + Assertions.assertThat(mappedValue.getClass()).isEqualTo(String.class); + } + + @Test + void getMappedValueValidMetaDataFloat() { + Field field = VectorSchemaRootUtils.generateFloatField("test"); + Double value = 1.2; + Object mappedValue = VectorSchemaRootConverter.INSTANCE.getMappedValue(field, value); + Assertions.assertThat(mappedValue).isEqualTo(value); + Assertions.assertThat(mappedValue.getClass()).isEqualTo(Double.class); + } + + @Test + void getMappedValueInvalidMetaDataFloat() { + Field field = VectorSchemaRootUtils.generateInvalidFloatField("test"); + String value = "1.2"; + Object mappedValue = VectorSchemaRootConverter.INSTANCE.getMappedValue(field, value); + Assertions.assertThat(mappedValue).isEqualTo(value); + Assertions.assertThat(mappedValue.getClass()).isEqualTo(String.class); + } + + @Test + void getMappedValueValidMetaDataString() { + Field field = VectorSchemaRootUtils.generateStringField("test"); + String value = "string"; + Object mappedValue = VectorSchemaRootConverter.INSTANCE.getMappedValue(field, value); + Assertions.assertThat(mappedValue).isEqualTo(value); + Assertions.assertThat(mappedValue.getClass()).isEqualTo(String.class); + } + + @Test + void getMappedValueInvalidMetaDataString() { + Field field = VectorSchemaRootUtils.generateInvalidStringField("test"); + Double value = 1.1; + Object mappedValue = VectorSchemaRootConverter.INSTANCE.getMappedValue(field, value); + Assertions.assertThat(mappedValue).isEqualTo(value); + Assertions.assertThat(mappedValue.getClass()).isEqualTo(Double.class); + } + + @Test + void getMappedValueValidMetaDataBoolean() { + Field field = VectorSchemaRootUtils.generateBoolField("test"); + Boolean value = true; + Object mappedValue = VectorSchemaRootConverter.INSTANCE.getMappedValue(field, value); + Assertions.assertThat(mappedValue).isEqualTo(value); + Assertions.assertThat(mappedValue.getClass()).isEqualTo(Boolean.class); + } + + @Test + void getMappedValueInvalidMetaDataBoolean() { + Field field = VectorSchemaRootUtils.generateInvalidBoolField("test"); + String value = "true"; + Object mappedValue = VectorSchemaRootConverter.INSTANCE.getMappedValue(field, value); + Assertions.assertThat(mappedValue).isEqualTo(value); + Assertions.assertThat(mappedValue.getClass()).isEqualTo(String.class); + } + + @Test + public void testConverterWithMetaType() { + try (VectorSchemaRoot root = VectorSchemaRootUtils.generateVectorSchemaRoot()) { + PointValues pointValues = VectorSchemaRootConverter.INSTANCE.toPointValues(0, root.getFieldVectors()); + + String measurement = pointValues.getMeasurement(); + Assertions.assertThat(measurement).isEqualTo("host"); + Assertions.assertThat(measurement.getClass()).isEqualTo(String.class); + + BigInteger expected = BigInteger.valueOf(123_456L * 1_000_000); + Assertions.assertThat((BigInteger) pointValues.getTimestamp()).isEqualByComparingTo(expected); + + Long memTotal = (Long) pointValues.getField("mem_total"); + Assertions.assertThat(memTotal).isEqualTo(2048); + Assertions.assertThat(memTotal.getClass()).isEqualTo(Long.class); + + Long diskFree = (Long) pointValues.getField("disk_free"); + Assertions.assertThat(diskFree).isEqualTo(1_000_000); + Assertions.assertThat(diskFree.getClass()).isEqualTo(Long.class); + + Double temperature = (Double) pointValues.getField("temperature"); + Assertions.assertThat(temperature).isEqualTo(100.8766f); + Assertions.assertThat(temperature.getClass()).isEqualTo(Double.class); + + String name = (String) pointValues.getField("name"); + Assertions.assertThat(name).isEqualTo("intel"); + Assertions.assertThat(name.getClass()).isEqualTo(String.class); + + Boolean isActive = (Boolean) pointValues.getField("isActive"); + Assertions.assertThat(isActive).isEqualTo(true); + Assertions.assertThat(isActive.getClass()).isEqualTo(Boolean.class); + } + } + @Test void timestampWithoutMetadataAndFieldWithoutMetadata() { FieldType timeType = new FieldType(true, new ArrowType.Time(TimeUnit.MILLISECOND, 32), null); @@ -133,7 +235,7 @@ void timestampWithoutMetadataAndFieldWithoutMetadata() { FieldType stringType = new FieldType(true, new ArrowType.Utf8(), null); Field field1 = new Field("field1", stringType, null); - try (VectorSchemaRoot root = initializeVectorSchemaRoot(timeField, field1)) { + try (VectorSchemaRoot root = VectorSchemaRootUtils.initializeVectorSchemaRoot(timeField, field1)) { // // set data @@ -152,7 +254,7 @@ void timestampWithoutMetadataAndFieldWithoutMetadata() { timeVector.setValueCount(1); root.setRowCount(1); - PointValues pointValues = VectorSchemaRootConverter.INSTANCE.toPointValues(0, root, root.getFieldVectors()); + PointValues pointValues = VectorSchemaRootConverter.INSTANCE.toPointValues(0, root.getFieldVectors()); BigInteger expected = BigInteger.valueOf(123_456L * 1_000_000); Assertions.assertThat((BigInteger) pointValues.getTimestamp()).isEqualByComparingTo(expected); @@ -165,7 +267,7 @@ public void measurementValue() { FieldType stringType = new FieldType(true, new ArrowType.Utf8(), null); Field measurementField = new Field("measurement", stringType, null); - try (VectorSchemaRoot root = initializeVectorSchemaRoot(measurementField)) { + try (VectorSchemaRoot root = VectorSchemaRootUtils.initializeVectorSchemaRoot(measurementField)) { // // set data @@ -180,7 +282,7 @@ public void measurementValue() { measurementVector.setValueCount(1); root.setRowCount(1); - PointValues pointValues = VectorSchemaRootConverter.INSTANCE.toPointValues(0, root, root.getFieldVectors()); + PointValues pointValues = VectorSchemaRootConverter.INSTANCE.toPointValues(0, root.getFieldVectors()); Assertions.assertThat(pointValues.getMeasurement()).isEqualTo("measurementValue"); } @@ -224,17 +326,6 @@ private VectorSchemaRoot createVectorSchemaRoot(@Nonnull final ArrowType arrowTy FieldType timeType = new FieldType(true, arrowType, null, metadata); Field timeField = new Field("timestamp", timeType, null); - return initializeVectorSchemaRoot(timeField); - } - - @NotNull - private VectorSchemaRoot initializeVectorSchemaRoot(@Nonnull final Field... fields) { - - Schema schema = new Schema(Arrays.asList(fields)); - - VectorSchemaRoot root = VectorSchemaRoot.create(schema, new RootAllocator(Long.MAX_VALUE)); - root.allocateNew(); - - return root; + return VectorSchemaRootUtils.initializeVectorSchemaRoot(timeField); } } diff --git a/src/test/java/com/influxdb/v3/client/internal/VectorSchemaRootUtils.java b/src/test/java/com/influxdb/v3/client/internal/VectorSchemaRootUtils.java new file mode 100644 index 0000000..3557e03 --- /dev/null +++ b/src/test/java/com/influxdb/v3/client/internal/VectorSchemaRootUtils.java @@ -0,0 +1,243 @@ +/* + * The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.influxdb.v3.client.internal; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nonnull; + +import org.apache.arrow.memory.RootAllocator; +import org.apache.arrow.vector.BigIntVector; +import org.apache.arrow.vector.BitVector; +import org.apache.arrow.vector.Float8Vector; +import org.apache.arrow.vector.TimeMilliVector; +import org.apache.arrow.vector.VarCharVector; +import org.apache.arrow.vector.VectorSchemaRoot; +import org.apache.arrow.vector.types.FloatingPointPrecision; +import org.apache.arrow.vector.types.TimeUnit; +import org.apache.arrow.vector.types.pojo.ArrowType; +import org.apache.arrow.vector.types.pojo.Field; +import org.apache.arrow.vector.types.pojo.FieldType; +import org.apache.arrow.vector.types.pojo.Schema; + +public final class VectorSchemaRootUtils { + + private VectorSchemaRootUtils() { } + + public static VectorSchemaRoot generateInvalidVectorSchemaRoot() { + Field testField = generateInvalidIntField("test_field"); + Field testField1 = generateInvalidUnsignedIntField("test_field1"); + Field testField2 = generateInvalidFloatField("test_field2"); + Field testField3 = generateInvalidStringField("test_field3"); + Field testField4 = generateInvalidBoolField("test_field4"); + + List fields = List.of(testField, + testField1, + testField2, + testField3, + testField4); + + VectorSchemaRoot root = initializeVectorSchemaRoot(fields.toArray(new Field[0])); + + VarCharVector intVector = (VarCharVector) root.getVector("test_field"); + intVector.allocateNew(); + intVector.set(0, "aaaa".getBytes()); + + VarCharVector uIntVector = (VarCharVector) root.getVector("test_field1"); + uIntVector.allocateNew(); + uIntVector.set(0, "aaaa".getBytes()); + + VarCharVector floatVector = (VarCharVector) root.getVector("test_field2"); + floatVector.allocateNew(); + floatVector.set(0, "aaaa".getBytes()); + + Float8Vector stringVector = (Float8Vector) root.getVector("test_field3"); + stringVector.allocateNew(); + stringVector.set(0, 100.2); + + VarCharVector booleanVector = (VarCharVector) root.getVector("test_field4"); + booleanVector.allocateNew(); + booleanVector.set(0, "aaa".getBytes()); + + return root; + } + + public static VectorSchemaRoot generateVectorSchemaRoot() { + Field measurementField = generateStringField("measurement"); + Field timeField = generateTimeField(); + Field memTotalField = generateIntField("mem_total"); + Field diskFreeField = generateUnsignedIntField("disk_free"); + Field temperatureField = generateFloatField("temperature"); + Field nameField = generateStringField("name"); + Field isActiveField = generateBoolField("isActive"); + List fields = List.of(measurementField, + timeField, + memTotalField, + diskFreeField, + temperatureField, + nameField, + isActiveField); + + VectorSchemaRoot root = initializeVectorSchemaRoot(fields.toArray(new Field[0])); + VarCharVector measurement = (VarCharVector) root.getVector("measurement"); + measurement.allocateNew(); + measurement.set(0, "host".getBytes()); + + TimeMilliVector timeVector = (TimeMilliVector) root.getVector("time"); + timeVector.allocateNew(); + timeVector.setSafe(0, 123_456); + + BigIntVector intVector = (BigIntVector) root.getVector("mem_total"); + intVector.allocateNew(); + intVector.set(0, 2048); + + BigIntVector unsignedIntVector = (BigIntVector) root.getVector("disk_free"); + unsignedIntVector.allocateNew(); + unsignedIntVector.set(0, 1_000_000); + + Float8Vector floatVector = (Float8Vector) root.getVector("temperature"); + floatVector.allocateNew(); + floatVector.set(0, 100.8766f); + + VarCharVector stringVector = (VarCharVector) root.getVector("name"); + stringVector.allocateNew(); + stringVector.setSafe(0, "intel".getBytes()); + + BitVector boolVector = (BitVector) root.getVector("isActive"); + boolVector.allocateNew(); + boolVector.setSafe(0, 1); + + return root; + } + + @Nonnull + public static VectorSchemaRoot initializeVectorSchemaRoot(@Nonnull final Field... fields) { + + Schema schema = new Schema(Arrays.asList(fields)); + + VectorSchemaRoot root = VectorSchemaRoot.create(schema, new RootAllocator(Long.MAX_VALUE)); + root.allocateNew(); + + return root; + } + + public static Field generateIntField(final String fieldName) { + Map metadata = new HashMap<>(); + metadata.put("iox::column::type", "iox::column_type::field::integer"); + FieldType intType = new FieldType(true, + new ArrowType.Int(64, true), + null, + metadata); + return new Field(fieldName, intType, null); + } + + public static Field generateInvalidIntField(final String fieldName) { + Map metadata = new HashMap<>(); + metadata.put("iox::column::type", "iox::column_type::field::integer"); + FieldType intType = new FieldType(true, + new ArrowType.Utf8(), + null, + metadata); + return new Field(fieldName, intType, null); + } + + public static Field generateUnsignedIntField(final String fieldName) { + Map metadata = new HashMap<>(); + metadata.put("iox::column::type", "iox::column_type::field::uinteger"); + FieldType intType = new FieldType(true, + new ArrowType.Int(64, true), + null, + metadata); + return new Field(fieldName, intType, null); + } + + public static Field generateInvalidUnsignedIntField(final String fieldName) { + Map metadata = new HashMap<>(); + metadata.put("iox::column::type", "iox::column_type::field::uinteger"); + FieldType intType = new FieldType(true, + new ArrowType.Utf8(), + null, + metadata); + return new Field(fieldName, intType, null); + } + + public static Field generateFloatField(final String fieldName) { + Map metadata = new HashMap<>(); + metadata.put("iox::column::type", "iox::column_type::field::float"); + FieldType floatType = new FieldType(true, + new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE), + null, + metadata); + return new Field(fieldName, floatType, null); + } + + public static Field generateInvalidFloatField(final String fieldName) { + Map metadata = new HashMap<>(); + metadata.put("iox::column::type", "iox::column_type::field::float"); + FieldType floatType = new FieldType(true, + new ArrowType.Utf8(), + null, + metadata); + return new Field(fieldName, floatType, null); + } + + public static Field generateStringField(final String fieldName) { + Map metadata = new HashMap<>(); + metadata.put("iox::column::type", "iox::column_type::field::string"); + FieldType stringType = new FieldType(true, new ArrowType.Utf8(), null, metadata); + return new Field(fieldName, stringType, null); + } + + public static Field generateInvalidStringField(final String fieldName) { + Map metadata = new HashMap<>(); + metadata.put("iox::column::type", "iox::column_type::field::string"); + FieldType stringType = new FieldType(true, + new ArrowType.FloatingPoint( + FloatingPointPrecision.DOUBLE), + null, + metadata); + return new Field(fieldName, stringType, null); + } + + public static Field generateBoolField(final String fieldName) { + Map metadata = new HashMap<>(); + metadata.put("iox::column::type", "iox::column_type::field::boolean"); + FieldType boolType = new FieldType(true, new ArrowType.Bool(), null, metadata); + return new Field(fieldName, boolType, null); + } + + public static Field generateInvalidBoolField(final String fieldName) { + Map metadata = new HashMap<>(); + metadata.put("iox::column::type", "iox::column_type::field::boolean"); + FieldType boolType = new FieldType(true, new ArrowType.Utf8(), null, metadata); + return new Field(fieldName, boolType, null); + } + + public static Field generateTimeField() { + FieldType timeType = new FieldType(true, + new ArrowType.Time(TimeUnit.MILLISECOND, 32), + null); + return new Field("time", timeType, null); + } +}