Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions README.md

This file was deleted.

30 changes: 30 additions & 0 deletions src/main/java/com/imprint/core/ImprintRecord.java
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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);
Expand Down
61 changes: 61 additions & 0 deletions src/main/java/com/imprint/core/ImprintRecordBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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); }
}

Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -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;
Expand Down
66 changes: 55 additions & 11 deletions src/main/java/com/imprint/types/ImprintDeserializers.java
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to settle on and add a style file

Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we check buffer.remaining here?

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");
}
Expand Down
65 changes: 60 additions & 5 deletions src/main/java/com/imprint/types/ImprintSerializers.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -55,7 +57,6 @@ public static void serializeBytes(byte[] value, ImprintBuffer buffer) {
public static void serializeArray(List<?> list, ImprintBuffer buffer, Function<Object, TypeCode> typeConverter, BiConsumer<Object, ImprintBuffer> elementSerializer)
throws ImprintException {
buffer.putVarInt(list.size());

if (list.isEmpty())
return; // Empty arrays technically don't need type code

Expand Down Expand Up @@ -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) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@agavra I think we had mentioned that timestamp should be milliseconds and that would be milliseconds since midnight, but Avro does milliseconds since Unix epoch and I think that's what we wanted to do here too which means this would need to be 8 bytes after all. If that's the case then I can go back to the main readme and update it again and clarify - just wanted to double check this again to make sure I wasn't getting myself too confused

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we said that timestamp would be 8 bytes millis since midnight but "time" (which is time of day) would be millis since midnight.

EDIT: I just looked at the README and didn't notice that we changed both to 4 bytes. Good catch, "timestamp" should definitely be 8 bytes. I'll change that in the core README as well.

long millisSinceEpoch = value.toEpochMilli();
buffer.putLong(millisSinceEpoch);
}

public static int estimateSize(TypeCode typeCode, Object value) {
byte code = typeCode.getCode();
Expand All @@ -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);
}
}
11 changes: 8 additions & 3 deletions src/main/java/com/imprint/types/TypeCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
/**
* Type codes for Imprint values.
*/
@Getter
public enum TypeCode {
NULL(0x0),
BOOL(0x1),
Expand All @@ -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()) {
Expand Down
Loading