diff --git a/api/src/main/java/io/kafbat/ui/util/jsonschema/JsonAvroConversion.java b/api/src/main/java/io/kafbat/ui/util/jsonschema/JsonAvroConversion.java index de23d40cd..7b65e04ad 100644 --- a/api/src/main/java/io/kafbat/ui/util/jsonschema/JsonAvroConversion.java +++ b/api/src/main/java/io/kafbat/ui/util/jsonschema/JsonAvroConversion.java @@ -462,6 +462,24 @@ enum LogicalTypeConversion { Map.of(FORMAT, new TextNode(DATE_TIME)))) ), + TIMESTAMP_NANOS("timestamp-nanos", + (node, schema) -> { + if (node.isIntegralNumber()) { + long nanosFromEpoch = node.longValue(); + long epochSeconds = nanosFromEpoch / 1_000_000_000L; + long nanoAdjustment = nanosFromEpoch % 1_000_000_000L; + return Instant.ofEpochSecond(epochSeconds, nanoAdjustment); + } else if (node.isTextual()) { + return Instant.parse(node.asText()); + } else { + throw new JsonAvroConversionException("node '%s' can't be converted to timestamp-nanos logical type" + .formatted(node)); + } + }, + (obj, schema) -> new TextNode(obj.toString()), + new SimpleFieldSchema(new SimpleJsonType(JsonType.Type.STRING, Map.of(FORMAT, new TextNode(DATE_TIME)))) + ), + LOCAL_TIMESTAMP_MILLIS("local-timestamp-millis", (node, schema) -> { if (node.isTextual()) { @@ -491,6 +509,18 @@ enum LogicalTypeConversion { new SimpleJsonType( JsonType.Type.STRING, Map.of(FORMAT, new TextNode(DATE_TIME)))) + ), + + LOCAL_TIMESTAMP_NANOS("local-timestamp-nanos", + (node, schema) -> { + if (node.isTextual()) { + return LocalDateTime.parse(node.asText()); + } + Instant instant = (Instant) TIMESTAMP_NANOS.jsonToAvroConversion.apply(node, schema); + return LocalDateTime.ofInstant(instant, ZoneOffset.UTC); + }, + (obj, schema) -> new TextNode(obj.toString()), + new SimpleFieldSchema(new SimpleJsonType(JsonType.Type.STRING, Map.of(FORMAT, new TextNode(DATE_TIME)))) ); private final String name; diff --git a/api/src/test/java/io/kafbat/ui/serdes/builtin/sr/SchemaRegistrySerdeTest.java b/api/src/test/java/io/kafbat/ui/serdes/builtin/sr/SchemaRegistrySerdeTest.java index d66a8d004..4778fec18 100644 --- a/api/src/test/java/io/kafbat/ui/serdes/builtin/sr/SchemaRegistrySerdeTest.java +++ b/api/src/test/java/io/kafbat/ui/serdes/builtin/sr/SchemaRegistrySerdeTest.java @@ -12,11 +12,19 @@ import io.kafbat.ui.util.jsonschema.JsonAvroConversion; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.math.BigDecimal; import java.nio.ByteBuffer; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneOffset; import java.util.List; import java.util.Map; +import java.util.UUID; import lombok.SneakyThrows; import net.bytebuddy.utility.RandomString; +import org.apache.avro.generic.GenericData; import org.apache.avro.generic.GenericDatumWriter; import org.apache.avro.io.Encoder; import org.apache.avro.io.EncoderFactory; @@ -341,6 +349,10 @@ void avroLogicalTypesRepresentationIsConsistentForSerializationAndDeserializatio "name": "lt_timestamp_micros", "type": { "type": "long", "logicalType": "timestamp-micros" } }, + { + "name": "lt_timestamp_nanos", + "type": { "type": "long", "logicalType": "timestamp-nanos" } + }, { "name": "lt_local_timestamp_millis", "type": { "type": "long", "logicalType": "local-timestamp-millis" } @@ -348,27 +360,36 @@ void avroLogicalTypesRepresentationIsConsistentForSerializationAndDeserializatio { "name": "lt_local_timestamp_micros", "type": { "type": "long", "logicalType": "local-timestamp-micros" } + }, + { + "name": "lt_local_timestamp_nanos", + "type": { "type": "long", "logicalType": "local-timestamp-nanos" } } ] }""" ); - String jsonPayload = """ - { - "lt_date":"1991-08-14", - "lt_decimal": 2.1617413862327545E11, - "lt_time_millis": "10:15:30.001", - "lt_time_micros": "10:15:30.123456", - "lt_uuid": "a37b75ca-097c-5d46-6119-f0637922e908", - "lt_timestamp_millis": "2007-12-03T10:15:30.123Z", - "lt_timestamp_micros": "2007-12-03T10:15:30.123456Z", - "lt_local_timestamp_millis": "2017-12-03T10:15:30.123", - "lt_local_timestamp_micros": "2017-12-03T10:15:30.123456" - } - """; + Instant instant = Instant.parse("2007-12-03T10:15:30.123456789Z"); + long timestampNanos = instant.getEpochSecond() * 1_000_000_000L + instant.getNano(); + + instant = LocalDateTime.parse("2017-12-03T10:15:30.123456789").toInstant(ZoneOffset.UTC); + long localTimestampNanos = instant.getEpochSecond() * 1_000_000_000L + instant.getNano(); + + GenericData.Record inputRecord = new GenericData.Record(schema.rawSchema()); + inputRecord.put("lt_date", LocalDate.of(1991, 8, 14)); + inputRecord.put("lt_uuid", UUID.fromString("a37b75ca-097c-5d46-6119-f0637922e908")); + inputRecord.put("lt_decimal", new BigDecimal("2.16")); + inputRecord.put("lt_time_millis", LocalTime.parse("10:15:30.001")); + inputRecord.put("lt_time_micros", LocalTime.parse("10:15:30.123456")); + inputRecord.put("lt_timestamp_millis", Instant.parse("2007-12-03T10:15:30.123Z")); + inputRecord.put("lt_timestamp_micros", Instant.parse("2007-12-03T10:15:30.123456Z")); + inputRecord.put("lt_timestamp_nanos", timestampNanos); + inputRecord.put("lt_local_timestamp_millis", LocalDateTime.parse("2017-12-03T10:15:30.123")); + inputRecord.put("lt_local_timestamp_micros", LocalDateTime.parse("2017-12-03T10:15:30.123456")); + inputRecord.put("lt_local_timestamp_nanos", localTimestampNanos); registryClient.register("test-value", schema); - assertSerdeCycle("test", jsonPayload); + assertSerdeCycle("test", inputRecord.toString()); } // 1. serialize input json to binary diff --git a/api/src/test/java/io/kafbat/ui/util/jsonschema/JsonAvroConversionTest.java b/api/src/test/java/io/kafbat/ui/util/jsonschema/JsonAvroConversionTest.java index 01e31875e..9e5c74c35 100644 --- a/api/src/test/java/io/kafbat/ui/util/jsonschema/JsonAvroConversionTest.java +++ b/api/src/test/java/io/kafbat/ui/util/jsonschema/JsonAvroConversionTest.java @@ -384,6 +384,10 @@ void logicalTypesField() { "name": "lt_timestamp_micros", "type": { "type": "long", "logicalType": "timestamp-micros" } }, + { + "name": "lt_timestamp_nanos", + "type": { "type": "long", "logicalType": "timestamp-nanos" } + }, { "name": "lt_local_timestamp_millis", "type": { "type": "long", "logicalType": "local-timestamp-millis" } @@ -391,6 +395,10 @@ void logicalTypesField() { { "name": "lt_local_timestamp_micros", "type": { "type": "long", "logicalType": "local-timestamp-micros" } + }, + { + "name": "lt_local_timestamp_nanos", + "type": { "type": "long", "logicalType": "local-timestamp-nanos" } } ] }""" @@ -405,8 +413,10 @@ void logicalTypesField() { "lt_uuid": "a37b75ca-097c-5d46-6119-f0637922e908", "lt_timestamp_millis": "2007-12-03T10:15:30.123Z", "lt_timestamp_micros": "2007-12-13T10:15:30.123456Z", + "lt_timestamp_nanos": "2007-12-13T10:15:30.123456789Z", "lt_local_timestamp_millis": "2017-12-03T10:15:30.123", - "lt_local_timestamp_micros": "2017-12-13T10:15:30.123456" + "lt_local_timestamp_micros": "2017-12-13T10:15:30.123456", + "lt_local_timestamp_nanos": "2017-12-13T10:15:30.123456789" } """; @@ -427,10 +437,14 @@ var record = (GenericData.Record) converted; .isEqualTo(Instant.parse("2007-12-03T10:15:30.123Z")); assertThat(record.get("lt_timestamp_micros")) .isEqualTo(Instant.parse("2007-12-13T10:15:30.123456Z")); + assertThat(record.get("lt_timestamp_nanos")) + .isEqualTo(Instant.parse("2007-12-13T10:15:30.123456789Z")); assertThat(record.get("lt_local_timestamp_millis")) .isEqualTo(LocalDateTime.parse("2017-12-03T10:15:30.123")); assertThat(record.get("lt_local_timestamp_micros")) .isEqualTo(LocalDateTime.parse("2017-12-13T10:15:30.123456")); + assertThat(record.get("lt_local_timestamp_nanos")) + .isEqualTo(LocalDateTime.parse("2017-12-13T10:15:30.123456789")); } } @@ -582,6 +596,10 @@ void logicalTypesField() { "name": "lt_timestamp_micros", "type": { "type": "long", "logicalType": "timestamp-micros" } }, + { + "name": "lt_timestamp_nanos", + "type": { "type": "long", "logicalType": "timestamp-nanos" } + }, { "name": "lt_local_timestamp_millis", "type": { "type": "long", "logicalType": "local-timestamp-millis" } @@ -589,6 +607,10 @@ void logicalTypesField() { { "name": "lt_local_timestamp_micros", "type": { "type": "long", "logicalType": "local-timestamp-micros" } + }, + { + "name": "lt_local_timestamp_nanos", + "type": { "type": "long", "logicalType": "local-timestamp-nanos" } } ] }""" @@ -602,8 +624,10 @@ void logicalTypesField() { inputRecord.put("lt_time_micros", LocalTime.parse("10:15:30.123456")); inputRecord.put("lt_timestamp_millis", Instant.parse("2007-12-03T10:15:30.123Z")); inputRecord.put("lt_timestamp_micros", Instant.parse("2007-12-13T10:15:30.123456Z")); + inputRecord.put("lt_timestamp_nanos", Instant.parse("2007-12-13T10:15:30.123456789Z")); inputRecord.put("lt_local_timestamp_millis", LocalDateTime.parse("2017-12-03T10:15:30.123")); inputRecord.put("lt_local_timestamp_micros", LocalDateTime.parse("2017-12-13T10:15:30.123456")); + inputRecord.put("lt_local_timestamp_nanos", LocalDateTime.parse("2017-12-13T10:15:30.123456789")); String expectedJson = """ { @@ -614,8 +638,10 @@ void logicalTypesField() { "lt_time_micros": "10:15:30.123456", "lt_timestamp_millis": "2007-12-03T10:15:30.123Z", "lt_timestamp_micros": "2007-12-13T10:15:30.123456Z", + "lt_timestamp_nanos": "2007-12-13T10:15:30.123456789Z", "lt_local_timestamp_millis": "2017-12-03T10:15:30.123", - "lt_local_timestamp_micros": "2017-12-13T10:15:30.123456" + "lt_local_timestamp_micros": "2017-12-13T10:15:30.123456", + "lt_local_timestamp_nanos": "2017-12-13T10:15:30.123456789" } """; diff --git a/pom.xml b/pom.xml index 064e17f13..9700fa6ef 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ 4.12.0 2.12.0 3.26.3 - 1.11.4 + 1.12.0 1.15.11 7.8.0 3.1.0