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