diff --git a/README.md b/README.md deleted file mode 100644 index 858c88a..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# imprint-java -Java implementation of the Imprint Serde diff --git a/src/main/java/com/imprint/core/ImprintRecord.java b/src/main/java/com/imprint/core/ImprintRecord.java index 35c2941..2221bc4 100644 --- a/src/main/java/com/imprint/core/ImprintRecord.java +++ b/src/main/java/com/imprint/core/ImprintRecord.java @@ -14,7 +14,12 @@ import lombok.ToString; import lombok.experimental.NonFinal; +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.*; +import java.util.UUID; @lombok.Value @EqualsAndHashCode(of = "serializedBytes") @@ -233,6 +238,26 @@ public ImprintRecord getRow(int fieldId) throws ImprintException { return (ImprintRecord) getTypedPrimitive(fieldId, com.imprint.types.TypeCode.ROW, "ROW"); } + public LocalDate getDate(int fieldId) throws ImprintException { + return (LocalDate) getTypedPrimitive(fieldId, com.imprint.types.TypeCode.DATE, "DATE"); + } + + public LocalTime getTime(int fieldId) throws ImprintException { + return (LocalTime) getTypedPrimitive(fieldId, com.imprint.types.TypeCode.TIME, "TIME"); + } + + public UUID getUuid(int fieldId) throws ImprintException { + return (UUID) getTypedPrimitive(fieldId, com.imprint.types.TypeCode.UUID, "UUID"); + } + + public BigDecimal getDecimal(int fieldId) throws ImprintException { + return (BigDecimal) getTypedPrimitive(fieldId, com.imprint.types.TypeCode.DECIMAL, "DECIMAL"); + } + + public Instant getTimestamp(int fieldId) throws ImprintException { + return (Instant) getTypedPrimitive(fieldId, com.imprint.types.TypeCode.TIMESTAMP, "TIMESTAMP"); + } + /** * Returns a copy of the bytes. */ @@ -590,6 +615,11 @@ private Object deserializePrimitive(com.imprint.types.TypeCode typeCode, Imprint case FLOAT64: case BYTES: case STRING: + case DATE: + case TIME: + case UUID: + case DECIMAL: + case TIMESTAMP: return ImprintDeserializers.deserializePrimitive(valueBuffer, typeCode); case ARRAY: return deserializePrimitiveArray(valueBuffer); diff --git a/src/main/java/com/imprint/core/ImprintRecordBuilder.java b/src/main/java/com/imprint/core/ImprintRecordBuilder.java index 3b77809..f4b1e3f 100644 --- a/src/main/java/com/imprint/core/ImprintRecordBuilder.java +++ b/src/main/java/com/imprint/core/ImprintRecordBuilder.java @@ -10,9 +10,14 @@ import lombok.SneakyThrows; import lombok.Value; +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.UUID; /** * A fluent builder for creating ImprintRecord instances with type-safe, @@ -57,6 +62,11 @@ static class FieldValue { static FieldValue ofBytes(byte[] value) { return new FieldValue(TypeCode.BYTES.getCode(), value); } static FieldValue ofArray(List value) { return new FieldValue(TypeCode.ARRAY.getCode(), value); } static FieldValue ofMap(Map value) { return new FieldValue(TypeCode.MAP.getCode(), value); } + static FieldValue ofDate(LocalDate value) { return new FieldValue(TypeCode.DATE.getCode(), value); } + static FieldValue ofTime(LocalTime value) { return new FieldValue(TypeCode.TIME.getCode(), value); } + static FieldValue ofUuid(UUID value) { return new FieldValue(TypeCode.UUID.getCode(), value); } + static FieldValue ofDecimal(BigDecimal value) { return new FieldValue(TypeCode.DECIMAL.getCode(), value); } + static FieldValue ofTimestamp(Instant value) { return new FieldValue(TypeCode.TIMESTAMP.getCode(), value); } static FieldValue ofNull() { return new FieldValue(TypeCode.NULL.getCode(), null); } } @@ -107,6 +117,27 @@ public ImprintRecordBuilder field(int id, Map map) { return addField(id, FieldValue.ofMap(map)); } + // New native types + public ImprintRecordBuilder field(int id, LocalDate value) { + return addField(id, FieldValue.ofDate(value)); + } + + public ImprintRecordBuilder field(int id, LocalTime value) { + return addField(id, FieldValue.ofTime(value)); + } + + public ImprintRecordBuilder field(int id, UUID value) { + return addField(id, FieldValue.ofUuid(value)); + } + + public ImprintRecordBuilder field(int id, BigDecimal value) { + return addField(id, FieldValue.ofDecimal(value)); + } + + public ImprintRecordBuilder field(int id, Instant value) { + return addField(id, FieldValue.ofTimestamp(value)); + } + // Nested records public ImprintRecordBuilder field(int id, ImprintRecord nestedRecord) { return addField(id, new FieldValue(TypeCode.ROW.getCode(), nestedRecord)); @@ -224,6 +255,21 @@ private FieldValue convertToFieldValue(Object obj) { if (obj instanceof ImprintRecord) { return new FieldValue(TypeCode.ROW.getCode(), obj); } + if (obj instanceof LocalDate) { + return FieldValue.ofDate((LocalDate) obj); + } + if (obj instanceof LocalTime) { + return FieldValue.ofTime((LocalTime) obj); + } + if (obj instanceof UUID) { + return FieldValue.ofUuid((UUID) obj); + } + if (obj instanceof BigDecimal) { + return FieldValue.ofDecimal((BigDecimal) obj); + } + if (obj instanceof Instant) { + return FieldValue.ofTimestamp((Instant) obj); + } throw new IllegalArgumentException("Unsupported type for auto-conversion: " + obj.getClass().getName()); } @@ -352,6 +398,21 @@ private void serializeFieldValue(FieldValue fieldValue, ImprintBuffer buffer) th case MAP: serializeMap((Map) value, buffer); break; + case DATE: + ImprintSerializers.serializeDate((LocalDate) value, buffer); + break; + case TIME: + ImprintSerializers.serializeTime((LocalTime) value, buffer); + break; + case UUID: + ImprintSerializers.serializeUuid((UUID) value, buffer); + break; + case DECIMAL: + ImprintSerializers.serializeDecimal((BigDecimal) value, buffer); + break; + case TIMESTAMP: + ImprintSerializers.serializeTimestamp((Instant) value, buffer); + break; case ROW: // Nested record serialization var nestedRecord = (ImprintRecord) value; diff --git a/src/main/java/com/imprint/types/ImprintDeserializers.java b/src/main/java/com/imprint/types/ImprintDeserializers.java index c69ee6d..8cb3333 100644 --- a/src/main/java/com/imprint/types/ImprintDeserializers.java +++ b/src/main/java/com/imprint/types/ImprintDeserializers.java @@ -6,7 +6,13 @@ import com.imprint.util.VarInt; import lombok.experimental.UtilityClass; -import java.nio.ByteBuffer; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.UUID; /** * Static primitive deserialization methods for all Imprint types. @@ -49,27 +55,65 @@ public static Object deserializePrimitive(ImprintBuffer buffer, TypeCode typeCod } return buffer.getDouble(); case BYTES: - VarInt.DecodeResult lengthResult = VarInt.decode(buffer); - int length = lengthResult.getValue(); - if (buffer.remaining() < length) { + int byteLength = VarInt.decode(buffer).getValue(); + if (buffer.remaining() < byteLength) { throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for bytes value data after VarInt. Needed: " + - length + ", available: " + buffer.remaining()); + byteLength + ", available: " + buffer.remaining()); } - byte[] bytes = new byte[length]; + byte[] bytes = new byte[byteLength]; buffer.get(bytes); return bytes; case STRING: - VarInt.DecodeResult strLengthResult = VarInt.decode(buffer); - int strLength = strLengthResult.getValue(); + int strLength = VarInt.decode(buffer).getValue(); if (buffer.remaining() < strLength) { - throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, - "Not enough bytes for string value data after VarInt. Needed: " + + throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, + "Not enough bytes for string value data after VarInt. Needed: " + strLength + ", available: " + buffer.remaining()); } byte[] stringBytes = new byte[strLength]; buffer.get(stringBytes); - return new String(stringBytes, java.nio.charset.StandardCharsets.UTF_8); + return new String(stringBytes, StandardCharsets.UTF_8); + case DATE: + if (buffer.remaining() < 4) { + throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for date"); + } + int daysSinceEpoch = buffer.getInt(); + return LocalDate.ofEpochDay(daysSinceEpoch); + case TIME: + if (buffer.remaining() < 4) { + throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for time"); + } + int millisSinceMidnight = buffer.getInt(); + return LocalTime.ofNanoOfDay(millisSinceMidnight * 1_000_000L); + case UUID: + if (buffer.remaining() < 16) { + throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for UUID"); + } + long mostSignificantBits = buffer.getLong(); + long leastSignificantBits = buffer.getLong(); + return new UUID(mostSignificantBits, leastSignificantBits); + case DECIMAL: + if (buffer.remaining() < 1) { + throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for decimal scale"); + } + int scale = VarInt.decode(buffer).getValue(); + int unscaledLength = VarInt.decode(buffer).getValue(); + if (buffer.remaining() < unscaledLength) { + throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, + "Not enough bytes for decimal unscaled value. Needed: " + + unscaledLength + ", available: " + buffer.remaining()); + } + byte[] unscaledBytes = new byte[unscaledLength]; + buffer.get(unscaledBytes); + var unscaledValue = new BigInteger(unscaledBytes); + return new BigDecimal(unscaledValue, scale); + case TIMESTAMP: + if (buffer.remaining() < 8) { + throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for timestamp"); + } + long millisSinceEpoch = buffer.getLong(); + return Instant.ofEpochMilli(millisSinceEpoch); default: throw new ImprintException(ErrorType.SERIALIZATION_ERROR, "Cannot deserialize " + typeCode + " as primitive"); } diff --git a/src/main/java/com/imprint/types/ImprintSerializers.java b/src/main/java/com/imprint/types/ImprintSerializers.java index 9ff338b..e5672ee 100644 --- a/src/main/java/com/imprint/types/ImprintSerializers.java +++ b/src/main/java/com/imprint/types/ImprintSerializers.java @@ -3,18 +3,20 @@ import com.imprint.error.ErrorType; import com.imprint.error.ImprintException; import com.imprint.util.ImprintBuffer; +import com.imprint.util.VarInt; import lombok.experimental.UtilityClass; +import java.math.BigDecimal; import java.nio.ByteBuffer; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.function.BiConsumer; import java.util.function.Function; -/** - * Static serialization methods for all Imprint types. - * Eliminates virtual dispatch overhead from TypeHandler interface. - */ @UtilityClass public final class ImprintSerializers { @@ -55,7 +57,6 @@ public static void serializeBytes(byte[] value, ImprintBuffer buffer) { public static void serializeArray(List list, ImprintBuffer buffer, Function typeConverter, BiConsumer elementSerializer) throws ImprintException { buffer.putVarInt(list.size()); - if (list.isEmpty()) return; // Empty arrays technically don't need type code @@ -139,6 +140,49 @@ public static void serializeNull(ImprintBuffer buffer) { // NULL values have no payload data } + /** + * Serialize a LocalDate as days since Unix epoch. + */ + public static void serializeDate(LocalDate value, ImprintBuffer buffer) { + int daysSinceEpoch = (int) value.toEpochDay(); + buffer.putInt(daysSinceEpoch); + } + + /** + * Serialize a LocalTime as milliseconds since midnight. + */ + public static void serializeTime(LocalTime value, ImprintBuffer buffer) { + int millisSinceMidnight = (int) (value.toNanoOfDay() / 1_000_000); + buffer.putInt(millisSinceMidnight); + } + + /** + * Serialize a UUID as 16-byte binary representation. + */ + public static void serializeUuid(UUID value, ImprintBuffer buffer) { + buffer.putLong(value.getMostSignificantBits()); + buffer.putLong(value.getLeastSignificantBits()); + } + + /** + * Serialize a BigDecimal as scale + unscaled value. + */ + public static void serializeDecimal(BigDecimal value, ImprintBuffer buffer) { + int scale = value.scale(); + byte[] unscaledBytes = value.unscaledValue().toByteArray(); + + buffer.putVarInt(scale); + buffer.putVarInt(unscaledBytes.length); + buffer.putBytes(unscaledBytes); + } + + /** + * Serialize an Instant as milliseconds since Unix epoch (UTC). + */ + public static void serializeTimestamp(Instant value, ImprintBuffer buffer) { + long millisSinceEpoch = value.toEpochMilli(); + buffer.putLong(millisSinceEpoch); + } public static int estimateSize(TypeCode typeCode, Object value) { byte code = typeCode.getCode(); @@ -156,6 +200,17 @@ public static int estimateSize(TypeCode typeCode, Object value) { } if (code == TypeCode.ARRAY.getCode() || code == TypeCode.MAP.getCode()) return 512; if (code == TypeCode.ROW.getCode()) return 1024; + if (code == TypeCode.DATE.getCode()) return 4; + if (code == TypeCode.TIME.getCode()) return 4; + if (code == TypeCode.UUID.getCode()) return 16; + if (code == TypeCode.TIMESTAMP.getCode()) return 8; + if (code == TypeCode.DECIMAL.getCode()) { + var decimal = (BigDecimal) value; + byte[] unscaledBytes = decimal.unscaledValue().toByteArray(); + return VarInt.encodedLength(decimal.scale()) + + VarInt.encodedLength(unscaledBytes.length) + + unscaledBytes.length; + } throw new IllegalArgumentException("Unknown TypeCode: " + typeCode); } } \ No newline at end of file diff --git a/src/main/java/com/imprint/types/TypeCode.java b/src/main/java/com/imprint/types/TypeCode.java index 3304d11..90bec46 100644 --- a/src/main/java/com/imprint/types/TypeCode.java +++ b/src/main/java/com/imprint/types/TypeCode.java @@ -7,6 +7,7 @@ /** * Type codes for Imprint values. */ +@Getter public enum TypeCode { NULL(0x0), BOOL(0x1), @@ -18,12 +19,16 @@ public enum TypeCode { STRING(0x7), ARRAY(0x8), MAP(0x9), - ROW(0xA); // TODO: implement (basically a placeholder for user-defined type) + ROW(0xA), + DATE(0xB), + TIME(0xC), + TIMESTAMP(0xD), + UUID(0xE), + DECIMAL(0xF); - @Getter private final byte code; - private static final TypeCode[] LOOKUP = new TypeCode[11]; + private static final TypeCode[] LOOKUP = new TypeCode[16]; static { for (var type : values()) { diff --git a/src/test/java/com/imprint/IntegrationTest.java b/src/test/java/com/imprint/IntegrationTest.java index 2b21f8e..a430831 100644 --- a/src/test/java/com/imprint/IntegrationTest.java +++ b/src/test/java/com/imprint/IntegrationTest.java @@ -6,7 +6,13 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.temporal.ChronoUnit; import java.util.*; +import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; @@ -878,4 +884,144 @@ void testBytesToBytesEquivalence() throws ImprintException { assertEquals(objectProjected.getString(1), bytesProjectedRecord.getString(1)); assertEquals(objectProjected.getInt32(3), bytesProjectedRecord.getInt32(3)); } + + @Test + @DisplayName("Native Types: Date, Time, UUID, Decimal, Timestamp round-trip") + void testNativeTypesRoundTrip() throws ImprintException { + var schemaId = new SchemaId(200, 0x12345678); + + LocalDate testDate = LocalDate.of(2023, 12, 25); + LocalTime testTime = LocalTime.now().truncatedTo(ChronoUnit.MILLIS); // Current time with millisecond precision + UUID testUuid = UUID.fromString("550e8400-e29b-41d4-a716-446655440000"); + BigDecimal testDecimal = new BigDecimal("123.456789"); + Instant testTimestamp = Instant.now().truncatedTo(ChronoUnit.MILLIS); + + var record = ImprintRecord.builder(schemaId) + .field(1, testDate) + .field(2, testTime) + .field(3, testUuid) + .field(4, testDecimal) + .field(5, testTimestamp) + .build(); + + assertEquals(testDate, record.getDate(1)); + assertEquals(testTime, record.getTime(2)); + assertEquals(testUuid, record.getUuid(3)); + assertEquals(testDecimal, record.getDecimal(4)); + assertEquals(testTimestamp, record.getTimestamp(5)); + assertEquals(5, record.getFieldCount()); + + // Test serialization round-trip + var deserialized = serializeAndDeserialize(record); + assertEquals(testDate, deserialized.getDate(1)); + assertEquals(testTime, deserialized.getTime(2)); + assertEquals(testUuid, deserialized.getUuid(3)); + assertEquals(testDecimal, deserialized.getDecimal(4)); + assertEquals(testTimestamp, deserialized.getTimestamp(5)); + } + + @Test + @DisplayName("Native Types: Edge cases and boundary values") + void testNativeTypesEdgeCases() throws ImprintException { + var schemaId = new SchemaId(201, 0x87654321); + + // Test edge values + LocalDate minDate = LocalDate.of(1970, 1, 1); // Unix epoch + LocalDate maxDate = LocalDate.of(9999, 12, 31); + LocalTime minTime = LocalTime.MIN; // 00:00:00 + LocalTime maxTime = LocalTime.MAX.truncatedTo(ChronoUnit.MILLIS); // Maximum time with millisecond precision + UUID nilUuid = UUID.fromString("00000000-0000-0000-0000-000000000000"); + BigDecimal largeDecimal = new BigDecimal("99999999999999999999.999999999999999999"); + BigDecimal smallDecimal = new BigDecimal("0.000000000000000001"); + Instant epochTimestamp = Instant.ofEpochMilli(0); // Unix epoch + Instant futureTimestamp = Instant.ofEpochMilli(4102444800000L); // Year 2100 + + var record = ImprintRecord.builder(schemaId) + .field(1, minDate) + .field(2, maxDate) + .field(3, minTime) + .field(4, maxTime) + .field(5, nilUuid) + .field(6, largeDecimal) + .field(7, smallDecimal) + .field(8, epochTimestamp) + .field(9, futureTimestamp) + .build(); + + var deserialized = serializeAndDeserialize(record); + assertEquals(minDate, deserialized.getDate(1)); + assertEquals(maxDate, deserialized.getDate(2)); + assertEquals(minTime, deserialized.getTime(3)); + assertEquals(maxTime, deserialized.getTime(4)); + assertEquals(nilUuid, deserialized.getUuid(5)); + assertEquals(largeDecimal, deserialized.getDecimal(6)); + assertEquals(smallDecimal, deserialized.getDecimal(7)); + assertEquals(epochTimestamp, deserialized.getTimestamp(8)); + assertEquals(futureTimestamp, deserialized.getTimestamp(9)); + } + + @Test + @DisplayName("Native Types: Auto-conversion in fieldIfNotNull") + void testNativeTypesAutoConversion() throws ImprintException { + var schemaId = new SchemaId(202, 0xabcdef01); + + LocalDate date = LocalDate.now(); + LocalTime time = LocalTime.now().truncatedTo(ChronoUnit.MILLIS); // Truncate to millisecond precision + UUID uuid = UUID.randomUUID(); + BigDecimal decimal = new BigDecimal("42.42"); + Instant timestamp = Instant.now().truncatedTo(ChronoUnit.MILLIS); + + // Test that the auto-conversion in fieldIfNotNull works + var record = ImprintRecord.builder(schemaId) + .fieldIfNotNull(1, date) + .fieldIfNotNull(2, time) + .fieldIfNotNull(3, uuid) + .fieldIfNotNull(4, decimal) + .fieldIfNotNull(5, timestamp) + .fieldIfNotNull(6, null) // Should not add this field + .build(); + + assertEquals(5, record.getFieldCount()); + assertEquals(date, record.getDate(1)); + assertEquals(time, record.getTime(2)); + assertEquals(uuid, record.getUuid(3)); + assertEquals(decimal, record.getDecimal(4)); + assertEquals(timestamp, record.getTimestamp(5)); + assertFalse(record.hasField(6)); + } + + @Test + @DisplayName("Native Types: Operations with new types") + void testNativeTypesOperations() throws ImprintException { + var schemaId = new SchemaId(203, 0xfedcba98); + + var record1 = ImprintRecord.builder(schemaId) + .field(1, LocalDate.of(2023, 1, 1)) + .field(2, LocalTime.of(12, 0, 0)) + .field(3, Instant.ofEpochMilli(1672531200000L)) // 2023-01-01T00:00:00Z + .build(); + + var record2 = ImprintRecord.builder(schemaId) + .field(4, UUID.randomUUID()) + .field(5, new BigDecimal("100.00")) + .build(); + + // Test merge with native types + var merged = record1.merge(record2); + assertEquals(5, merged.getFieldCount()); + assertEquals(LocalDate.of(2023, 1, 1), merged.getDate(1)); + assertEquals(LocalTime.of(12, 0, 0), merged.getTime(2)); + assertEquals(Instant.ofEpochMilli(1672531200000L), merged.getTimestamp(3)); + assertNotNull(merged.getUuid(4)); + assertEquals(new BigDecimal("100.00"), merged.getDecimal(5)); + + // Test project with native types + var projected = merged.project(1, 3, 5); + assertEquals(3, projected.getFieldCount()); + assertEquals(LocalDate.of(2023, 1, 1), projected.getDate(1)); + assertEquals(Instant.ofEpochMilli(1672531200000L), projected.getTimestamp(3)); + assertEquals(new BigDecimal("100.00"), projected.getDecimal(5)); + assertFalse(projected.hasField(2)); + assertFalse(projected.hasField(4)); + } } \ No newline at end of file diff --git a/src/test/java/com/imprint/core/ImprintRecordTest.java b/src/test/java/com/imprint/core/ImprintRecordTest.java index 562f5fd..44bec5c 100644 --- a/src/test/java/com/imprint/core/ImprintRecordTest.java +++ b/src/test/java/com/imprint/core/ImprintRecordTest.java @@ -6,6 +6,13 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.temporal.ChronoUnit; +import java.util.UUID; + import static org.junit.jupiter.api.Assertions.*; @DisplayName("ImprintRecord") @@ -201,35 +208,6 @@ void shouldPreserveSerializedBytes() { } } - @Nested - @DisplayName("Performance Characteristics") - class PerformanceCharacteristics { - - @Test - @DisplayName("should have minimal memory footprint") - void shouldHaveMinimalMemoryFootprint() { - var originalSize = testRecord.serializeToBuffer().remaining(); - var serializedSize = serializedRecord.getSerializedSize(); - - assertEquals(originalSize, serializedSize); - - // ImprintRecord should not significantly increase memory usage - // (just the wrapper object itself) - assertTrue(serializedSize > 0); - } - - @Test - @DisplayName("should support repeated operations efficiently") - void shouldSupportRepeatedOperationsEfficiently() throws ImprintException { - // Multiple field access should not cause performance degradation - for (int i = 0; i < 100; i++) { - assertEquals(Integer.valueOf(42), serializedRecord.getInt32(1)); - assertEquals("hello", serializedRecord.getString(2)); - assertTrue(serializedRecord.hasField(3)); - } - } - } - @Nested @DisplayName("Edge Cases") class EdgeCases { @@ -286,4 +264,83 @@ void shouldNotBeEqualForDifferentData() throws ImprintException { assertNotEquals(serializedRecord, differentSerialized); } } + + @Nested + @DisplayName("Native Type Getters") + class NativeTypeGetters { + + private ImprintRecord nativeTypesRecord; + private LocalTime testTime; + private Instant testTimestamp; + + @BeforeEach + void setUp() throws ImprintException { + testTime = LocalTime.now().truncatedTo(ChronoUnit.MILLIS); + testTimestamp = Instant.now().truncatedTo(ChronoUnit.MILLIS); + + nativeTypesRecord = ImprintRecord.builder(testSchema) + .field(10, LocalDate.of(2023, 12, 25)) + .field(11, testTime) + .field(12, UUID.fromString("550e8400-e29b-41d4-a716-446655440000")) + .field(13, new BigDecimal("123.456789")) + .field(14, testTimestamp) + .build(); + } + + @Test + @DisplayName("should get Date values correctly") + void shouldGetDateValuesCorrectly() throws ImprintException { + assertEquals(LocalDate.of(2023, 12, 25), nativeTypesRecord.getDate(10)); + } + + @Test + @DisplayName("should get Time values correctly") + void shouldGetTimeValuesCorrectly() throws ImprintException { + assertEquals(testTime, nativeTypesRecord.getTime(11)); + } + + @Test + @DisplayName("should get UUID values correctly") + void shouldGetUuidValuesCorrectly() throws ImprintException { + assertEquals(UUID.fromString("550e8400-e29b-41d4-a716-446655440000"), nativeTypesRecord.getUuid(12)); + } + + @Test + @DisplayName("should get Decimal values correctly") + void shouldGetDecimalValuesCorrectly() throws ImprintException { + assertEquals(new BigDecimal("123.456789"), nativeTypesRecord.getDecimal(13)); + } + + @Test + @DisplayName("should get Timestamp values correctly") + void shouldGetTimestampValuesCorrectly() throws ImprintException { + assertEquals(testTimestamp, nativeTypesRecord.getTimestamp(14)); + } + + @Test + @DisplayName("should throw exception for type mismatch on native types") + void shouldThrowExceptionForTypeMismatch() { + // Try to get a Date as Time + assertThrows(ImprintException.class, () -> nativeTypesRecord.getTime(10)); + + // Try to get a UUID as Decimal + assertThrows(ImprintException.class, () -> nativeTypesRecord.getDecimal(12)); + + // Try to get a Timestamp as Date + assertThrows(ImprintException.class, () -> nativeTypesRecord.getDate(14)); + + // Try to get a string as Date (from the original test record) + assertThrows(ImprintException.class, () -> testRecord.getDate(2)); + } + + @Test + @DisplayName("should throw exception for non-existent native type fields") + void shouldThrowExceptionForNonExistentNativeTypeFields() { + assertThrows(ImprintException.class, () -> nativeTypesRecord.getDate(99)); + assertThrows(ImprintException.class, () -> nativeTypesRecord.getTime(99)); + assertThrows(ImprintException.class, () -> nativeTypesRecord.getUuid(99)); + assertThrows(ImprintException.class, () -> nativeTypesRecord.getDecimal(99)); + assertThrows(ImprintException.class, () -> nativeTypesRecord.getTimestamp(99)); + } + } } \ No newline at end of file