diff --git a/src/jmh/java/com/imprint/benchmark/ComparisonBenchmark.java b/src/jmh/java/com/imprint/benchmark/ComparisonBenchmark.java index f47da20..d8fbcde 100644 --- a/src/jmh/java/com/imprint/benchmark/ComparisonBenchmark.java +++ b/src/jmh/java/com/imprint/benchmark/ComparisonBenchmark.java @@ -16,7 +16,7 @@ @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Benchmark) @Warmup(iterations = 3, time = 1) -@Measurement(iterations = 7, time = 1) +@Measurement(iterations = 25, time = 1) @Fork(value = 1, jvmArgs = {"-Xms4g", "-Xmx4g"}) public class ComparisonBenchmark { @@ -30,7 +30,7 @@ public class ComparisonBenchmark { new KryoSerializingBenchmark(), new MessagePackSerializingBenchmark()); - @Param({"Imprint", "Jackson-JSON", "Protobuf", "FlatBuffers", "Avro-Generic", "Thrift", "Kryo", "MessagePack", "CapnProto"}) + @Param({"Imprint"}) public String framework; private SerializingBenchmark serializingBenchmark; @@ -60,12 +60,12 @@ public void deserialize(Blackhole bh) { serializingBenchmark.deserialize(bh); } - @Benchmark + //@Benchmark public void projectAndSerialize(Blackhole bh) { serializingBenchmark.projectAndSerialize(bh); } - @Benchmark + //@Benchmark public void mergeAndSerialize(Blackhole bh) { serializingBenchmark.mergeAndSerialize(bh); } diff --git a/src/jmh/java/com/imprint/benchmark/FieldAccessBenchmark.java b/src/jmh/java/com/imprint/benchmark/FieldAccessBenchmark.java deleted file mode 100644 index 06a7717..0000000 --- a/src/jmh/java/com/imprint/benchmark/FieldAccessBenchmark.java +++ /dev/null @@ -1,275 +0,0 @@ -package com.imprint.benchmark; - -import com.imprint.core.ImprintRecord; -import com.imprint.core.ImprintRecordBuilder; -import com.imprint.core.SchemaId; -import com.imprint.types.MapKey; -import com.imprint.types.Value; -import org.openjdk.jmh.annotations.*; -import org.openjdk.jmh.infra.Blackhole; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.concurrent.TimeUnit; - -/** - * Benchmarks for ImprintRecord field access and projection operations. - * Tests the zero-copy field access performance claims. - */ -@BenchmarkMode(Mode.AverageTime) -@OutputTimeUnit(TimeUnit.NANOSECONDS) -@State(Scope.Benchmark) -@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) -@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) -@Fork(1) -public class FieldAccessBenchmark { - - private ImprintRecord sparseRecord; - private ImprintRecord denseRecord; - private ImprintRecord largeRecord; - - // Field IDs for testing different access patterns - private int[] firstFields; - private int[] middleFields; - private int[] lastFields; - private int[] randomFields; - private int[] allFields; - - @Setup - public void setup() throws Exception { - sparseRecord = createSparseRecord(); // Few fields, large field IDs - denseRecord = createDenseRecord(); // Many sequential fields - largeRecord = createLargeRecord(); // Large record with complex data - - // Setup field access patterns - firstFields = new int[]{1, 2, 3, 4, 5}; - middleFields = new int[]{45, 46, 47, 48, 49}; - lastFields = new int[]{95, 96, 97, 98, 99}; - randomFields = new int[]{7, 23, 41, 67, 89}; - allFields = new int[100]; - for (int i = 0; i < 100; i++) { - allFields[i] = i + 1; - } - } - - // ===== SINGLE FIELD ACCESS BENCHMARKS ===== - - @Benchmark - public void accessFirstField(Blackhole bh) throws Exception { - var value = denseRecord.getValue(1); - bh.consume(value); - } - - @Benchmark - public void accessMiddleField(Blackhole bh) throws Exception { - var value = denseRecord.getValue(50); - bh.consume(value); - } - - @Benchmark - public void accessLastField(Blackhole bh) throws Exception { - var value = denseRecord.getValue(100); - bh.consume(value); - } - - @Benchmark - public void accessNonExistentField(Blackhole bh) throws Exception { - var value = denseRecord.getValue(999); - bh.consume(value); - } - - // ===== MULTIPLE FIELD ACCESS PATTERNS ===== - - @Benchmark - public void accessFirstFields(Blackhole bh) throws Exception { - for (int fieldId : firstFields) { - var value = denseRecord.getValue(fieldId); - bh.consume(value); - } - } - - @Benchmark - public void accessMiddleFields(Blackhole bh) throws Exception { - for (int fieldId : middleFields) { - var value = denseRecord.getValue(fieldId); - bh.consume(value); - } - } - - @Benchmark - public void accessLastFields(Blackhole bh) throws Exception { - for (int fieldId : lastFields) { - var value = denseRecord.getValue(fieldId); - bh.consume(value); - } - } - - @Benchmark - public void accessRandomFields(Blackhole bh) throws Exception { - for (int fieldId : randomFields) { - var value = denseRecord.getValue(fieldId); - bh.consume(value); - } - } - - // ===== FIELD PROJECTION BENCHMARKS ===== - - @Benchmark - public void projectSmallSubset(Blackhole bh) throws Exception { - // Project 5 fields from a 100-field record - var projection = simulateProject(denseRecord, firstFields); - bh.consume(projection); - } - - @Benchmark - public void projectMediumSubset(Blackhole bh) throws Exception { - // Project 25 fields from a 100-field record - int[] fields = Arrays.copyOf(allFields, 25); - var projection = simulateProject(denseRecord, fields); - bh.consume(projection); - } - - @Benchmark - public void projectLargeSubset(Blackhole bh) throws Exception { - // Project 75 fields from a 100-field record - int[] fields = Arrays.copyOf(allFields, 75); - var projection = simulateProject(denseRecord, fields); - bh.consume(projection); - } - - @Benchmark - public void projectAllFields(Blackhole bh) throws Exception { - // Project all fields (should be nearly equivalent to full record) - var projection = simulateProject(denseRecord, allFields); - bh.consume(projection); - } - - // ===== RAW BYTES ACCESS BENCHMARKS ===== - - @Benchmark - public void getRawBytesFirstField(Blackhole bh) { - var rawBytes = denseRecord.getRawBytes(1); - bh.consume(rawBytes); - } - - @Benchmark - public void getRawBytesMiddleField(Blackhole bh) { - var rawBytes = denseRecord.getRawBytes(50); - bh.consume(rawBytes); - } - - @Benchmark - public void getRawBytesLastField(Blackhole bh) { - var rawBytes = denseRecord.getRawBytes(100); - bh.consume(rawBytes); - } - - // ===== SPARSE VS DENSE ACCESS PATTERNS ===== - - @Benchmark - public void accessSparseRecord(Blackhole bh) throws Exception { - // Access fields in sparse record (large field IDs, few fields) - var value1 = sparseRecord.getValue(1000); - var value2 = sparseRecord.getValue(5000); - var value3 = sparseRecord.getValue(10000); - bh.consume(value1); - bh.consume(value2); - bh.consume(value3); - } - - @Benchmark - public void accessDenseRecord(Blackhole bh) throws Exception { - // Access fields in dense record (sequential field IDs) - var value1 = denseRecord.getValue(1); - var value2 = denseRecord.getValue(2); - var value3 = denseRecord.getValue(3); - bh.consume(value1); - bh.consume(value2); - bh.consume(value3); - } - - // ===== HELPER METHODS ===== - - /** - * Simulates field projection by creating a new record with only specified fields. - * This should be replaced with actual project API when available. - */ - private ImprintRecord simulateProject(ImprintRecord source, int[] fieldIds) throws Exception { - var builder = ImprintRecord.builder(source.getHeader().getSchemaId()); - - for (int fieldId : fieldIds) { - var value = source.getValue(fieldId); - if (value != null) { - builder.field(fieldId, value); - } - } - - return builder.build(); - } - - private ImprintRecord createSparseRecord() throws Exception { - return ImprintRecord.builder(new SchemaId(1, 0x12345678)) - .field(1000, Value.fromString("sparse_field_1")) - .field(5000, Value.fromInt32(42)) - .field(10000, Value.fromFloat64(3.14159)) - .field(15000, Value.fromBoolean(true)) - .field(20000, Value.fromString("sparse_field_5")) - .build(); - } - - private ImprintRecord createDenseRecord() throws Exception { - var builder = ImprintRecord.builder(new SchemaId(2, 0x87654321)); - - // Dense record with 100 sequential fields - for (int i = 1; i <= 100; i++) { - switch (i % 5) { - case 0: - builder.field(i, Value.fromString("string_field_" + i)); - break; - case 1: - builder.field(i, Value.fromInt32(i * 10)); - break; - case 2: - builder.field(i, Value.fromFloat64(i * 1.5)); - break; - case 3: - builder.field(i, Value.fromBoolean(i % 2 == 0)); - break; - case 4: - builder.field(i, Value.fromInt64(i * 1000L)); - break; - } - } - - return builder.build(); - } - - private ImprintRecord createLargeRecord() throws Exception { - var builder = ImprintRecord.builder(new SchemaId(3, 0xABCDEF12)); - - // Large record with complex fields (arrays, maps) - builder.field(1, Value.fromString("Large record with complex data")); - - // Add a large array - var list = new ArrayList(); - for (int i = 0; i < 200; i++) { - list.add(Value.fromInt32(i)); - } - builder.field(2, Value.fromArray(list)); - - // Add a large map - var map = new HashMap(); - for (int i = 0; i < 100; i++) { - map.put(MapKey.fromString("key_" + i), Value.fromString("value_" + i)); - } - builder.field(3, Value.fromMap(map)); - - // Add more fields - for (int i = 4; i <= 50; i++) { - builder.field(i, Value.fromBytes(new byte[1024])); // 1KB byte arrays - } - - return builder.build(); - } -} \ No newline at end of file diff --git a/src/jmh/java/com/imprint/benchmark/ImprintDetailedBenchmark.java b/src/jmh/java/com/imprint/benchmark/ImprintDetailedBenchmark.java new file mode 100644 index 0000000..c9d4514 --- /dev/null +++ b/src/jmh/java/com/imprint/benchmark/ImprintDetailedBenchmark.java @@ -0,0 +1,103 @@ +package com.imprint.benchmark; + +import com.imprint.core.ImprintRecord; +import com.imprint.core.ImprintRecordBuilder; +import com.imprint.core.SchemaId; +import com.imprint.error.ImprintException; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.concurrent.TimeUnit; + +/** + * Detailed breakdown of Imprint serialization performance to identify bottlenecks. + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +@Warmup(iterations = 3, time = 1) +@Measurement(iterations = 20, time = 1) +@Fork(value = 1, jvmArgs = {"-Xms4g", "-Xmx4g"}) +public class ImprintDetailedBenchmark { + + private DataGenerator.TestRecord testData; + private ImprintRecordBuilder preBuiltBuilder; + private ImprintRecord preBuiltRecord; + private static final SchemaId SCHEMA_ID = new SchemaId(1, 1); + + @Setup(Level.Trial) + public void setup() { + testData = DataGenerator.createTestRecord(); + try { + preBuiltBuilder = buildRecord(testData); + preBuiltRecord = preBuiltBuilder.build(); + } catch (ImprintException e) { + throw new RuntimeException(e); + } + } + + private ImprintRecordBuilder buildRecord(DataGenerator.TestRecord pojo) { + var builder = ImprintRecord.builder(SCHEMA_ID, 8); // Pre-size for 8 fields + builder.field(0, pojo.id); + builder.field(1, pojo.timestamp); + builder.field(2, pojo.flags); + builder.field(3, pojo.active); + builder.field(4, pojo.value); + builder.field(5, pojo.data); + builder.field(6, pojo.tags); + builder.field(7, pojo.metadata); + return builder; + } + + @Benchmark + public void fieldAddition(Blackhole bh) { + // Benchmark: POJO → Builder (field addition only) + try { + var builder = buildRecord(testData); + bh.consume(builder); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Benchmark + public void buildToBuffer(Blackhole bh) { + // Benchmark: Builder → Bytes (serialization only) + try { + bh.consume(preBuiltBuilder.buildToBuffer()); + } catch (ImprintException e) { + throw new RuntimeException(e); + } + } + + @Benchmark + public void serializeToBuffer(Blackhole bh) { + // Benchmark: Record → Bytes (just buffer copy) + bh.consume(preBuiltRecord.serializeToBuffer()); + } + + @Benchmark + public void fullPipeline(Blackhole bh) { + // Benchmark: POJO → Builder → Bytes (complete pipeline) + try { + bh.consume(buildRecord(testData).buildToBuffer()); + } catch (ImprintException e) { + throw new RuntimeException(e); + } + } + + public static void main(String[] args) throws RunnerException { + Options opt = new OptionsBuilder() + .include(ImprintDetailedBenchmark.class.getSimpleName()) + .forks(1) + .mode(Mode.AverageTime) + .timeUnit(TimeUnit.NANOSECONDS) + .build(); + + new Runner(opt).run(); + } +} \ No newline at end of file diff --git a/src/jmh/java/com/imprint/benchmark/SerializationBenchmark.java b/src/jmh/java/com/imprint/benchmark/SerializationBenchmark.java deleted file mode 100644 index 51c9f48..0000000 --- a/src/jmh/java/com/imprint/benchmark/SerializationBenchmark.java +++ /dev/null @@ -1,180 +0,0 @@ -package com.imprint.benchmark; - -import com.imprint.core.ImprintRecord; -import com.imprint.core.ImprintRecordBuilder; -import com.imprint.core.SchemaId; -import com.imprint.types.MapKey; -import com.imprint.types.Value; -import org.openjdk.jmh.annotations.*; -import org.openjdk.jmh.infra.Blackhole; -import org.openjdk.jmh.runner.Runner; -import org.openjdk.jmh.runner.RunnerException; -import org.openjdk.jmh.runner.options.Options; -import org.openjdk.jmh.runner.options.OptionsBuilder; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.concurrent.TimeUnit; - -/** - * Benchmarks for ImprintRecord serialization and deserialization operations. - */ -@BenchmarkMode(Mode.AverageTime) -@OutputTimeUnit(TimeUnit.NANOSECONDS) -@State(Scope.Benchmark) -@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) -@Measurement(iterations = 7, time = 1, timeUnit = TimeUnit.SECONDS) -@Fork(1) -public class SerializationBenchmark { - - private ImprintRecord smallRecord; - private ImprintRecord mediumRecord; - private ImprintRecord largeRecord; - - private ByteBuffer smallRecordBytes; - private ByteBuffer mediumRecordBytes; - private ByteBuffer largeRecordBytes; - - @Setup - public void setup() throws Exception { - // Create test records of varying sizes for deserialization benchmarks - smallRecord = createSmallRecord().build(); - mediumRecord = createMediumRecord().build(); - largeRecord = createLargeRecord().build(); - - // Pre-serialize for deserialization benchmarks - smallRecordBytes = smallRecord.serializeToBuffer(); - mediumRecordBytes = mediumRecord.serializeToBuffer(); - largeRecordBytes = largeRecord.serializeToBuffer(); - } - - // ===== SERIALIZATION BENCHMARKS ===== - - @Benchmark - public void buildAndSerializeSmallRecord(Blackhole bh) throws Exception { - ByteBuffer result = createSmallRecord().buildToBuffer(); - bh.consume(result); - } - - @Benchmark - public void buildAndSerializeMediumRecord(Blackhole bh) throws Exception { - ByteBuffer result = createMediumRecord().buildToBuffer(); - bh.consume(result); - } - - @Benchmark - public void buildAndSerializeLargeRecord(Blackhole bh) throws Exception { - ByteBuffer result = createLargeRecord().buildToBuffer(); - bh.consume(result); - } - - // ===== DESERIALIZATION BENCHMARKS ===== - - @Benchmark - public void deserializeSmallRecord(Blackhole bh) throws Exception { - ImprintRecord result = ImprintRecord.deserialize(smallRecordBytes.duplicate()); - bh.consume(result); - } - - @Benchmark - public void deserializeMediumRecord(Blackhole bh) throws Exception { - ImprintRecord result = ImprintRecord.deserialize(mediumRecordBytes.duplicate()); - bh.consume(result); - } - - @Benchmark - public void deserializeLargeRecord(Blackhole bh) throws Exception { - ImprintRecord result = ImprintRecord.deserialize(largeRecordBytes.duplicate()); - bh.consume(result); - } - - // ===== HELPER METHODS ===== - - private ImprintRecordBuilder createSmallRecord() throws Exception { - // Small record: ~10 fields, simple types - return ImprintRecord.builder(new SchemaId(1, 0x12345678)) - .field(1, "Product") - .field(2, 12345) - .field(3, 99.99) - .field(4, true) - .field(5, "Electronics"); - } - - private ImprintRecordBuilder createMediumRecord() throws Exception { - var builder = ImprintRecord.builder(new SchemaId(1, 0x12345678)); - - // Medium record: ~50 fields, mixed types including arrays - builder.field(1, "Product"); - builder.field(2, 12345); - builder.field(3, 99.99); - builder.field(4, true); - builder.field(5, "Electronics"); - - // Add array field - var tags = Arrays.asList( - "popular", - "trending", - "bestseller" - ); - builder.field(6, tags); - - // Add map field (all string values for consistency) - var metadata = new HashMap(); - metadata.put("manufacturer", "TechCorp"); - metadata.put("model", "TC-2024"); - metadata.put("year", "2024"); - builder.field(7, metadata); - - // Add more fields for medium size - for (int i = 8; i <= 50; i++) { - builder.field(i, "field_" + i + "_value"); - } - - return builder; - } - - private ImprintRecordBuilder createLargeRecord() throws Exception { - var builder = ImprintRecord.builder(new SchemaId(1, 0x12345678)); - - // Large record: ~200 fields, complex nested structures - builder.field(1, "LargeProduct"); - builder.field(2, 12345); - builder.field(3, 99.99); - - // Large array - var largeArray = new ArrayList(); - for (int i = 0; i < 100; i++) { - largeArray.add("item_" + i); - } - builder.field(4, largeArray); - - // Large map - var largeMap = new HashMap(); - for (int i = 0; i < 50; i++) { - largeMap.put("key_" + i, "value_" + i); - } - builder.field(5, largeMap); - - // Many string fields - for (int i = 6; i <= 200; i++) { - builder.field(i, "this_is_a_longer_field_value_for_field_" + i + "_to_increase_record_size"); - } - - return builder; - } - - public static void main(String[] args) throws RunnerException { - Options opt = new OptionsBuilder() - .include(SerializationBenchmark.class.getSimpleName()) - .forks(1) - .warmupIterations(5) - .measurementIterations(5) - .mode(Mode.AverageTime) - .timeUnit(TimeUnit.NANOSECONDS) - .build(); - - new Runner(opt).run(); - } -} \ No newline at end of file diff --git a/src/jmh/java/com/imprint/benchmark/StringBenchmark.java b/src/jmh/java/com/imprint/benchmark/StringBenchmark.java deleted file mode 100644 index 045940e..0000000 --- a/src/jmh/java/com/imprint/benchmark/StringBenchmark.java +++ /dev/null @@ -1,316 +0,0 @@ -package com.imprint.benchmark; - -import com.imprint.core.ImprintRecord; -import com.imprint.core.SchemaId; -import com.imprint.types.Value; -import org.openjdk.jmh.annotations.*; -import org.openjdk.jmh.runner.Runner; -import org.openjdk.jmh.runner.options.OptionsBuilder; - -import java.nio.ByteBuffer; -import java.util.concurrent.TimeUnit; - -@BenchmarkMode(Mode.AverageTime) -@OutputTimeUnit(TimeUnit.NANOSECONDS) -@State(Scope.Benchmark) -@Fork(1) -@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) -@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) -@SuppressWarnings("unused") -public class StringBenchmark { - - private static final SchemaId SCHEMA_ID = new SchemaId(1, 42); - - // Small strings (typical field names, short values) - private String smallString5; - private String smallString20; - private String smallString50; - - // Medium strings (typical text content) - private String mediumString500; - private String mediumString2K; - - // Large strings (document content, JSON payloads) - private String largeString10K; - private String largeString100K; - private String largeString1M; - - // Pre-serialized records for deserialization benchmarks - private byte[] serializedSmall5; - private byte[] serializedSmall20; - private byte[] serializedSmall50; - private byte[] serializedMedium500; - private byte[] serializedMedium2K; - private byte[] serializedLarge10K; - private byte[] serializedLarge100K; - private byte[] serializedLarge1M; - - private ImprintRecord preDeserializedSmall5; - private ImprintRecord preDeserializedMedium500; - private ImprintRecord preDeserializedLarge100K; - - @Setup - public void setup() throws Exception { - // Generate strings of different sizes - smallString5 = generateString(5); - smallString20 = generateString(20); - smallString50 = generateString(50); - mediumString500 = generateString(500); - mediumString2K = generateString(2 * 1024); - largeString10K = generateString(10 * 1024); - largeString100K = generateString(100 * 1024); - largeString1M = generateString(1024 * 1024); - - // Pre-serialize records for deserialization benchmarks - serializedSmall5 = bufferToArray(createStringRecord(smallString5).serializeToBuffer()); - serializedSmall20 = bufferToArray(createStringRecord(smallString20).serializeToBuffer()); - serializedSmall50 = bufferToArray(createStringRecord(smallString50).serializeToBuffer()); - serializedMedium500 = bufferToArray(createStringRecord(mediumString500).serializeToBuffer()); - serializedMedium2K = bufferToArray(createStringRecord(mediumString2K).serializeToBuffer()); - serializedLarge10K = bufferToArray(createStringRecord(largeString10K).serializeToBuffer()); - serializedLarge100K = bufferToArray(createStringRecord(largeString100K).serializeToBuffer()); - serializedLarge1M = bufferToArray(createStringRecord(largeString1M).serializeToBuffer()); - - preDeserializedSmall5 = ImprintRecord.deserialize(serializedSmall5); - preDeserializedMedium500 = ImprintRecord.deserialize(serializedMedium500); - preDeserializedLarge100K = ImprintRecord.deserialize(serializedLarge100K); - } - - private String generateString(int length) { - StringBuilder sb = new StringBuilder(length); - String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 "; - for (int i = 0; i < length; i++) { - sb.append(chars.charAt(i % chars.length())); - } - return sb.toString(); - } - - private ImprintRecord createStringRecord(String value) throws Exception { - return ImprintRecord.builder(SCHEMA_ID) - .field(1, value) - .build(); - } - - private String extractString(Value value) { - if (value instanceof Value.StringValue) { - return ((Value.StringValue) value).getValue(); - } else if (value instanceof Value.StringBufferValue) { - return ((Value.StringBufferValue) value).getValue(); - } - return null; - } - - private byte[] bufferToArray(ByteBuffer buffer) { - byte[] array = new byte[buffer.remaining()]; - buffer.duplicate().get(array); - return array; - } - - // Serialization benchmarks - - @Benchmark - public ByteBuffer serializeSmallString5() throws Exception { - return createStringRecord(smallString5).serializeToBuffer(); - } - - @Benchmark - public ByteBuffer serializeSmallString20() throws Exception { - return createStringRecord(smallString20).serializeToBuffer(); - } - - @Benchmark - public ByteBuffer serializeSmallString50() throws Exception { - return createStringRecord(smallString50).serializeToBuffer(); - } - - @Benchmark - public ByteBuffer serializeMediumString500() throws Exception { - return createStringRecord(mediumString500).serializeToBuffer(); - } - - @Benchmark - public ByteBuffer serializeMediumString2K() throws Exception { - return createStringRecord(mediumString2K).serializeToBuffer(); - } - - @Benchmark - public ByteBuffer serializeLargeString10K() throws Exception { - return createStringRecord(largeString10K).serializeToBuffer(); - } - - @Benchmark - public ByteBuffer serializeLargeString100K() throws Exception { - return createStringRecord(largeString100K).serializeToBuffer(); - } - - @Benchmark - public ByteBuffer serializeLargeString1M() throws Exception { - return createStringRecord(largeString1M).serializeToBuffer(); - } - - // Deserialization benchmarks - - @Benchmark - public ImprintRecord deserializeSmallString5() throws Exception { - return ImprintRecord.deserialize(serializedSmall5); - } - - @Benchmark - public ImprintRecord deserializeSmallString20() throws Exception { - return ImprintRecord.deserialize(serializedSmall20); - } - - @Benchmark - public ImprintRecord deserializeSmallString50() throws Exception { - return ImprintRecord.deserialize(serializedSmall50); - } - - @Benchmark - public ImprintRecord deserializeMediumString500() throws Exception { - return ImprintRecord.deserialize(serializedMedium500); - } - - @Benchmark - public ImprintRecord deserializeMediumString2K() throws Exception { - return ImprintRecord.deserialize(serializedMedium2K); - } - - @Benchmark - public ImprintRecord deserializeLargeString10K() throws Exception { - return ImprintRecord.deserialize(serializedLarge10K); - } - - @Benchmark - public ImprintRecord deserializeLargeString100K() throws Exception { - return ImprintRecord.deserialize(serializedLarge100K); - } - - @Benchmark - public ImprintRecord deserializeLargeString1M() throws Exception { - return ImprintRecord.deserialize(serializedLarge1M); - } - - // String access benchmarks - - @Benchmark - public String accessSmallString5() throws Exception { - ImprintRecord record = ImprintRecord.deserialize(serializedSmall5); - Value value = record.getValue(1); - return value != null ? extractString(value) : null; - } - - @Benchmark - public String accessMediumString500() throws Exception { - ImprintRecord record = ImprintRecord.deserialize(serializedMedium500); - Value value = record.getValue(1); - return value != null ? extractString(value) : null; - } - - @Benchmark - public String accessLargeString100K() throws Exception { - ImprintRecord record = ImprintRecord.deserialize(serializedLarge100K); - Value value = record.getValue(1); - return value != null ? extractString(value) : null; - } - - // Raw bytes access benchmarks (zero-copy) - - @Benchmark - public ByteBuffer getRawBytesSmallString5() throws Exception { - ImprintRecord record = ImprintRecord.deserialize(serializedSmall5); - return record.getRawBytes(1); - } - - @Benchmark - public ByteBuffer getRawBytesMediumString500() throws Exception { - ImprintRecord record = ImprintRecord.deserialize(serializedMedium500); - return record.getRawBytes(1); - } - - @Benchmark - public ByteBuffer getRawBytesLargeString100K() throws Exception { - ImprintRecord record = ImprintRecord.deserialize(serializedLarge100K); - return record.getRawBytes(1); - } - - // Size measurement benchmarks - - @Benchmark - public int measureSmallString5Size() throws Exception { - return createStringRecord(smallString5).serializeToBuffer().remaining(); - } - - @Benchmark - public int measureMediumString500Size() throws Exception { - return createStringRecord(mediumString500).serializeToBuffer().remaining(); - } - - @Benchmark - public int measureLargeString100KSize() throws Exception { - return createStringRecord(largeString100K).serializeToBuffer().remaining(); - } - - // Pure string access benchmarks (no record deserialization overhead) - @Benchmark - public String pureStringAccessSmall5() throws Exception { - Value value = preDeserializedSmall5.getValue(1); - return value != null ? extractString(value) : null; - } - - @Benchmark - public String pureStringAccessMedium500() throws Exception { - Value value = preDeserializedMedium500.getValue(1); - return value != null ? extractString(value) : null; - } - - @Benchmark - public String pureStringAccessLarge100K() throws Exception { - Value value = preDeserializedLarge100K.getValue(1); - return value != null ? extractString(value) : null; - } - - // Test cached vs uncached access - @Benchmark - public String cachedStringAccessSmall5() throws Exception { - // Second access should hit cache - Value value1 = preDeserializedSmall5.getValue(1); - String result1 = value1 != null ? extractString(value1) : null; - Value value2 = preDeserializedSmall5.getValue(1); - return value2 != null ? extractString(value2) : null; - } - - public static void main(String[] args) throws Exception { - runDeserializationOnly(); - } - - public static void runAll() throws Exception { - var opt = new OptionsBuilder() - .include(StringBenchmark.class.getSimpleName()) - .build(); - new Runner(opt).run(); - } - - /** - * Run only string deserialization benchmarks to measure the impact of - * ThreadLocal buffer pool optimization and fast/fallback path performance. - */ - public static void runDeserializationOnly() throws Exception { - var opt = new OptionsBuilder() - .include(StringBenchmark.class.getSimpleName() + ".*deserialize.*") // Only deserialize methods - .forks(0) // Run in same JVM to avoid serialization issues - .build(); - new Runner(opt).run(); - } - - /** - * Run only pure string access benchmarks (no record deserialization overhead) - * to isolate string decode performance with ThreadLocal buffer optimization. - */ - public static void runStringAccessOnly() throws Exception { - var opt = new OptionsBuilder() - .include(StringBenchmark.class.getSimpleName() + ".*(pureStringAccess|cachedStringAccess).*") // Only pure string access methods - .forks(0) // Run in same JVM to avoid serialization issues - .build(); - new Runner(opt).run(); - } -} \ No newline at end of file diff --git a/src/jmh/java/com/imprint/benchmark/serializers/ImprintSerializingBenchmark.java b/src/jmh/java/com/imprint/benchmark/serializers/ImprintSerializingBenchmark.java index e71a5c0..490b9d2 100644 --- a/src/jmh/java/com/imprint/benchmark/serializers/ImprintSerializingBenchmark.java +++ b/src/jmh/java/com/imprint/benchmark/serializers/ImprintSerializingBenchmark.java @@ -12,6 +12,7 @@ public class ImprintSerializingBenchmark extends AbstractSerializingBenchmark { private ImprintRecord imprintRecord1; + private ImprintRecordBuilder preBuiltRecord; // Pre-built record for testing private byte[] serializedRecord1; private byte[] serializedRecord2; private static final SchemaId SCHEMA_ID = new SchemaId(1, 1); @@ -24,8 +25,9 @@ public ImprintSerializingBenchmark() { public void setup(DataGenerator.TestRecord testRecord, DataGenerator.TestRecord testRecord2) { super.setup(testRecord, testRecord2); try { - this.imprintRecord1 = buildRecord(testRecord); - ImprintRecord imprintRecord2 = buildRecord(testRecord2); + this.imprintRecord1 = buildRecord(testRecord).build(); + this.preBuiltRecord = buildRecord(testRecord); // Pre-built for testing + ImprintRecord imprintRecord2 = buildRecord(testRecord2).build(); ByteBuffer buf1 = this.imprintRecord1.serializeToBuffer(); this.serializedRecord1 = new byte[buf1.remaining()]; @@ -39,7 +41,7 @@ public void setup(DataGenerator.TestRecord testRecord, DataGenerator.TestRecord } } - private ImprintRecord buildRecord(DataGenerator.TestRecord pojo) throws ImprintException { + private ImprintRecordBuilder buildRecord(DataGenerator.TestRecord pojo) throws ImprintException { var builder = ImprintRecord.builder(SCHEMA_ID); builder.field(0, pojo.id); builder.field(1, pojo.timestamp); @@ -49,16 +51,23 @@ private ImprintRecord buildRecord(DataGenerator.TestRecord pojo) throws ImprintE builder.field(5, pojo.data); builder.field(6, pojo.tags); builder.field(7, pojo.metadata); - return builder.build(); + return builder; } @Override public void serialize(Blackhole bh) { + // Test 3: Just field addition (POJO → Builder) try { - bh.consume(buildRecord(DataGenerator.createTestRecord()).serializeToBuffer()); - } catch (ImprintException e) { - throw new RuntimeException(e); + var builder = buildRecord(this.testData); + bh.consume(builder); // Consume builder to prevent dead code elimination + } catch (ImprintException ignored) { } + + // Test 2: Just serialization (Builder → Bytes) + // try{ + // bh.consume(preBuiltRecord.buildToBuffer()); + // } catch (ImprintException ignored) { + // } } @Override diff --git a/src/main/java/com/imprint/core/ImprintFieldObjectMap.java b/src/main/java/com/imprint/core/ImprintFieldObjectMap.java index e0a63f0..a6e63de 100644 --- a/src/main/java/com/imprint/core/ImprintFieldObjectMap.java +++ b/src/main/java/com/imprint/core/ImprintFieldObjectMap.java @@ -12,7 +12,7 @@ * - Sort values in place and return without allocation (subsequently poisons the map) */ final class ImprintFieldObjectMap { - private static final int DEFAULT_CAPACITY = 512; + private static final int DEFAULT_CAPACITY = 64; private static final float LOAD_FACTOR = 0.75f; private static final short EMPTY_KEY = -1; // Reserved empty marker (field IDs are >= 0) @@ -48,6 +48,18 @@ public void put(int key, T value) { putValue((short) key, value); } + /** + * Put a value and return the previous value if any. + * @return the previous value, or null if no previous value existed + */ + public T putAndReturnOld(int key, T value) { + if (poisoned) + throw new IllegalStateException("Map is invalid after compaction - cannot perform operations"); + if (key > Short.MAX_VALUE) + throw new IllegalArgumentException("Field ID must be 0-" + Short.MAX_VALUE + ", got: " + key); + return putValueAndReturnOld((short) key, value); + } + private void putValue(short key, T value) { if (poisoned) throw new IllegalStateException("Map is invalid after compaction - cannot perform operations"); @@ -63,6 +75,28 @@ private void putValue(short key, T value) { keys[index] = key; values[index] = value; } + + @SuppressWarnings("unchecked") + private T putValueAndReturnOld(short key, T value) { + if (poisoned) + throw new IllegalStateException("Map is invalid after compaction - cannot perform operations"); + if (key < 0) + throw new IllegalArgumentException("Field ID must be 0-" + Short.MAX_VALUE + ", got: " + key); + + if (size >= threshold) + resize(); + int index = findSlot(key); + T oldValue = null; + if (keys[index] == EMPTY_KEY) { + size++; + } else { + // Existing key - capture old value + oldValue = (T) values[index]; + } + keys[index] = key; + values[index] = value; + return oldValue; + } @SuppressWarnings("unchecked") public T get(int key) { @@ -303,4 +337,36 @@ private static int nextPowerOfTwo(int n) { if (n <= 1) return 1; return Integer.highestOneBit(n - 1) << 1; } -} \ No newline at end of file + + /** + * Result holder for in-place sorted fields - returns both keys and values. + */ + public static final class SortedFieldsResult { + public final short[] keys; + public final Object[] values; + public final int count; + + SortedFieldsResult(short[] keys, Object[] values, int count) { + this.keys = keys; + this.values = values; + this.count = count; + } + } + + /** + * Get both keys and values sorted by key order with zero allocation. + * WARNING: Modifies internal state, and renders map operations unstable and in an illegal state. + */ + public SortedFieldsResult getSortedFields() { + if (size == 0) { + poisoned = true; + return new SortedFieldsResult(keys, values, 0); + } + + compactEntries(); + sortEntriesByKey(size); + poisoned = true; + + return new SortedFieldsResult(keys, values, size); + } +} diff --git a/src/main/java/com/imprint/core/ImprintRecord.java b/src/main/java/com/imprint/core/ImprintRecord.java index e6f9de6..a8a745d 100644 --- a/src/main/java/com/imprint/core/ImprintRecord.java +++ b/src/main/java/com/imprint/core/ImprintRecord.java @@ -4,8 +4,7 @@ import com.imprint.error.ErrorType; import com.imprint.error.ImprintException; import com.imprint.ops.ImprintOperations; -import com.imprint.types.TypeCode; -import com.imprint.types.Value; +import com.imprint.types.*; import com.imprint.util.VarInt; import lombok.AccessLevel; @@ -38,11 +37,11 @@ public class ImprintRecord { @Getter(AccessLevel.PUBLIC) Header header; - + @Getter(AccessLevel.PACKAGE) // Raw directory bytes (read-only) ByteBuffer directoryBuffer; - + @Getter(AccessLevel.PACKAGE) // Raw payload bytes ByteBuffer payload; @@ -51,7 +50,7 @@ public class ImprintRecord { @Getter(AccessLevel.NONE) //Directory View cache to allow for easier mutable operations needed for lazy initialization Directory.DirectoryView directoryView; - + /** * Package-private constructor for @Value that creates immutable ByteBuffer views. */ @@ -62,9 +61,9 @@ public class ImprintRecord { this.payload = payload.asReadOnlyBuffer(); this.directoryView = null; } - + // ========== STATIC FACTORY METHODS ========== - + /** * Create a builder for constructing new ImprintRecord instances. */ @@ -72,6 +71,15 @@ public static ImprintRecordBuilder builder(SchemaId schemaId) { return new ImprintRecordBuilder(schemaId); } + /** + * Create a pre-sized builder for constructing new ImprintRecord instances. + * @param schemaId Schema identifier + * @param expectedFieldCount Expected number of fields to optimize memory allocation + */ + public static ImprintRecordBuilder builder(SchemaId schemaId, int expectedFieldCount) { + return new ImprintRecordBuilder(schemaId, expectedFieldCount); + } + public static ImprintRecordBuilder builder(int fieldspaceId, int schemaHash) { return new ImprintRecordBuilder(new SchemaId(fieldspaceId, schemaHash)); } @@ -86,27 +94,27 @@ public static ImprintRecord deserialize(byte[] bytes) throws ImprintException { public static ImprintRecord deserialize(ByteBuffer buffer) throws ImprintException { return fromBytes(buffer); } - + /** * Create a ImprintRecord from complete serialized bytes. */ public static ImprintRecord fromBytes(ByteBuffer serializedBytes) throws ImprintException { Objects.requireNonNull(serializedBytes, "Serialized bytes cannot be null"); - + var buffer = serializedBytes.duplicate().order(ByteOrder.LITTLE_ENDIAN); - + // Parse header var header = parseHeader(buffer); - + // Extract directory and payload sections var parsedBuffers = parseBuffersFromSerialized(serializedBytes); - + return new ImprintRecord(serializedBytes, header, parsedBuffers.directoryBuffer, parsedBuffers.payload); } - - + + // ========== ZERO-COPY OPERATIONS ========== - + /** * Merge with another ImprintRecord using pure byte operations. * Results in a new ImprintRecord without any object creation. @@ -115,7 +123,7 @@ public ImprintRecord merge(ImprintRecord other) throws ImprintException { var mergedBytes = ImprintOperations.mergeBytes(this.serializedBytes, other.serializedBytes); return fromBytes(mergedBytes); } - + /** * Project fields using pure byte operations. * Results in a new ImprintRecord without any object creation. @@ -124,7 +132,7 @@ public ImprintRecord project(int... fieldIds) throws ImprintException { var projectedBytes = ImprintOperations.projectBytes(this.serializedBytes, fieldIds); return fromBytes(projectedBytes); } - + /** * Chain multiple operations efficiently. * Each operation works on bytes without creating intermediate objects. @@ -132,7 +140,7 @@ public ImprintRecord project(int... fieldIds) throws ImprintException { public ImprintRecord projectAndMerge(ImprintRecord other, int... projectFields) throws ImprintException { return this.project(projectFields).merge(other); } - + /** * Get the raw serialized bytes. * This is the most efficient way to pass the record around. @@ -140,7 +148,7 @@ public ImprintRecord projectAndMerge(ImprintRecord other, int... projectFields) public ByteBuffer getSerializedBytes() { return serializedBytes.duplicate(); } - + /** * Get a DirectoryView for straight through directory access. */ @@ -150,14 +158,14 @@ public Directory.DirectoryView getDirectoryView() { } return directoryView; } - + /** * Get the directory list. */ public List getDirectory() { return getDirectoryView().toList(); } - + /** * Get raw bytes for a field without deserializing. */ @@ -168,99 +176,84 @@ public ByteBuffer getRawBytes(int fieldId) { return null; } } - + /** * Get raw bytes for a field by short ID. */ public ByteBuffer getRawBytes(short fieldId) { return getRawBytes((int) fieldId); } - - /** - * Estimate the serialized size of this record. - */ - public int estimateSerializedSize() { - return serializedBytes.remaining(); - } - + /** - * Get a field value by ID. + * Get a field value by ID as Object. * Uses zero-copy binary search to locate the field. */ - public Value getValue(int fieldId) throws ImprintException { + public Object getValue(int fieldId) throws ImprintException { var entry = getDirectoryView().findEntry(fieldId); if (entry == null) return null; - + var fieldBuffer = getFieldBuffer(fieldId); if (fieldBuffer == null) return null; - - return deserializeValue(entry.getTypeCode(), fieldBuffer); + + return deserializePrimitive(entry.getTypeCode(), fieldBuffer); } - + /** * Check if a field exists without deserializing it. */ public boolean hasField(int fieldId) { return getDirectoryView().findEntry(fieldId) != null; } - + /** * Get the number of fields without parsing the directory. */ public int getFieldCount() { return getDirectoryCount(); } - + // ========== TYPED GETTERS ========== - + public String getString(int fieldId) throws ImprintException { - var value = getValidatedValue(fieldId, "STRING"); - if (value instanceof Value.StringValue) - return ((Value.StringValue) value).getValue(); - if (value instanceof Value.StringBufferValue) - return ((Value.StringBufferValue) value).getValue(); - throw new ImprintException(ErrorType.TYPE_MISMATCH, "Field " + fieldId + " is not a STRING"); - } - + return (String) getTypedPrimitive(fieldId, com.imprint.types.TypeCode.STRING, "STRING"); + } + public int getInt32(int fieldId) throws ImprintException { - return getTypedValueOrThrow(fieldId, com.imprint.types.TypeCode.INT32, Value.Int32Value.class, "int32").getValue(); + return (Integer) getTypedPrimitive(fieldId, com.imprint.types.TypeCode.INT32, "INT32"); } - + public long getInt64(int fieldId) throws ImprintException { - return getTypedValueOrThrow(fieldId, com.imprint.types.TypeCode.INT64, Value.Int64Value.class, "int64").getValue(); + return (Long) getTypedPrimitive(fieldId, com.imprint.types.TypeCode.INT64, "INT64"); } - + public boolean getBoolean(int fieldId) throws ImprintException { - return getTypedValueOrThrow(fieldId, com.imprint.types.TypeCode.BOOL, Value.BoolValue.class, "boolean").getValue(); + return (Boolean) getTypedPrimitive(fieldId, com.imprint.types.TypeCode.BOOL, "BOOL"); } - + public float getFloat32(int fieldId) throws ImprintException { - return getTypedValueOrThrow(fieldId, com.imprint.types.TypeCode.FLOAT32, Value.Float32Value.class, "float32").getValue(); + return (Float) getTypedPrimitive(fieldId, com.imprint.types.TypeCode.FLOAT32, "FLOAT32"); } - + public double getFloat64(int fieldId) throws ImprintException { - return getTypedValueOrThrow(fieldId, com.imprint.types.TypeCode.FLOAT64, Value.Float64Value.class, "float64").getValue(); + return (Double) getTypedPrimitive(fieldId, com.imprint.types.TypeCode.FLOAT64, "FLOAT64"); } - + public byte[] getBytes(int fieldId) throws ImprintException { - var value = getValidatedValue(fieldId, "BYTES"); - if (value instanceof Value.BytesValue) - return ((Value.BytesValue) value).getValue(); - if (value instanceof Value.BytesBufferValue) - return ((Value.BytesBufferValue) value).getValue(); - throw new ImprintException(ErrorType.TYPE_MISMATCH, "Field " + fieldId + " is not BYTES"); - } - - public java.util.List getArray(int fieldId) throws ImprintException { - return getTypedValueOrThrow(fieldId, com.imprint.types.TypeCode.ARRAY, Value.ArrayValue.class, "ARRAY").getValue(); - } - - public java.util.Map getMap(int fieldId) throws ImprintException { - return getTypedValueOrThrow(fieldId, com.imprint.types.TypeCode.MAP, Value.MapValue.class, "MAP").getValue(); - } - + return (byte[]) getTypedPrimitive(fieldId, com.imprint.types.TypeCode.BYTES, "BYTES"); + } + + @SuppressWarnings("unchecked") + public java.util.List getArray(int fieldId) throws ImprintException { + return (java.util.List) getTypedPrimitive(fieldId, com.imprint.types.TypeCode.ARRAY, "ARRAY"); + } + + @SuppressWarnings("unchecked") + public java.util.Map getMap(int fieldId) throws ImprintException { + return (java.util.Map) getTypedPrimitive(fieldId, com.imprint.types.TypeCode.MAP, "MAP"); + } + public ImprintRecord getRow(int fieldId) throws ImprintException { - return getTypedValueOrThrow(fieldId, com.imprint.types.TypeCode.ROW, Value.RowValue.class, "ROW").getValue(); + return (ImprintRecord) getTypedPrimitive(fieldId, com.imprint.types.TypeCode.ROW, "ROW"); } /** @@ -276,7 +269,7 @@ public ByteBuffer serializeToBuffer() { public SchemaId getSchemaId() { return header.getSchemaId(); } - + /** * Estimate the memory footprint of this record. */ @@ -284,50 +277,51 @@ public int getSerializedSize() { return serializedBytes.remaining(); } - + /** - * Get and validate a value exists and is not null. + * Get and validate a field exists, is not null, and has the expected type. */ - private Value getValidatedValue(int fieldId, String typeName) throws ImprintException { - var value = getValue(fieldId); - if (value == null) + private Object getTypedPrimitive(int fieldId, com.imprint.types.TypeCode expectedTypeCode, String typeName) throws ImprintException { + var entry = getDirectoryView().findEntry(fieldId); + if (entry == null) throw new ImprintException(ErrorType.FIELD_NOT_FOUND, "Field " + fieldId + " not found"); - if (value.getTypeCode() == com.imprint.types.TypeCode.NULL) + + if (entry.getTypeCode() == com.imprint.types.TypeCode.NULL) throw new ImprintException(ErrorType.TYPE_MISMATCH, "Field " + fieldId + " is NULL, cannot retrieve as " + typeName); - return value; - } + + if (entry.getTypeCode() != expectedTypeCode) + throw new ImprintException(ErrorType.TYPE_MISMATCH, "Field " + fieldId + " is of type " + entry.getTypeCode() + ", expected " + typeName); - private T getTypedValueOrThrow(int fieldId, com.imprint.types.TypeCode expectedTypeCode, Class expectedValueClass, String expectedTypeName) - throws ImprintException { - var value = getValidatedValue(fieldId, expectedTypeName); - if (value.getTypeCode() == expectedTypeCode && expectedValueClass.isInstance(value)) - return expectedValueClass.cast(value); - throw new ImprintException(ErrorType.TYPE_MISMATCH, "Field " + fieldId + " is of type " + value.getTypeCode() + ", expected " + expectedTypeName); + var fieldBuffer = getFieldBuffer(fieldId); + if (fieldBuffer == null) + throw new ImprintException(ErrorType.FIELD_NOT_FOUND, "Field " + fieldId + " buffer not found"); + + return deserializePrimitive(entry.getTypeCode(), fieldBuffer); } - + /** * Parse buffers from serialized record bytes. */ private static ParsedBuffers parseBuffersFromSerialized(ByteBuffer serializedRecord) throws ImprintException { var buffer = serializedRecord.duplicate().order(ByteOrder.LITTLE_ENDIAN); - - // Parse header and extract sections using shared utility + + // Parse header and extract sections using shared utility var header = parseHeaderFromBuffer(buffer); var sections = extractBufferSections(buffer, header); - + return new ParsedBuffers(sections.directoryBuffer, sections.payloadBuffer); } - + private static class ParsedBuffers { final ByteBuffer directoryBuffer; final ByteBuffer payload; - + ParsedBuffers(ByteBuffer directoryBuffer, ByteBuffer payload) { this.directoryBuffer = directoryBuffer; this.payload = payload; } } - + private int getDirectoryCount() { try { return VarInt.decode(directoryBuffer.duplicate()).getValue(); @@ -335,7 +329,7 @@ private int getDirectoryCount() { return 0; // Cache as 0 on error } } - + /** * Gets ByteBuffer view of a field's data. */ @@ -343,43 +337,43 @@ private ByteBuffer getFieldBuffer(int fieldId) throws ImprintException { var entry = findDirectoryEntry(fieldId); if (entry == null) return null; - + int startOffset = entry.getOffset(); int endOffset = findEndOffset(entry.getId()); - + if (startOffset < 0 || endOffset < 0 || startOffset > payload.limit() || endOffset > payload.limit() || startOffset > endOffset) { throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Invalid field buffer range: start=" + startOffset + ", end=" + endOffset); } - + var fieldBuffer = payload.duplicate(); fieldBuffer.position(startOffset).limit(endOffset); return fieldBuffer; } - + private Directory findDirectoryEntry(int fieldId) throws ImprintException { var searchBuffer = directoryBuffer.duplicate().order(ByteOrder.LITTLE_ENDIAN); - + int count = getDirectoryCount(); if (count == 0) return null; - + // Advance past varint to entries VarInt.decode(searchBuffer); int directoryStartPos = searchBuffer.position(); - + int low = 0; int high = count - 1; - + while (low <= high) { int mid = (low + high) >>> 1; int entryPos = directoryStartPos + (mid * Constants.DIR_ENTRY_BYTES); - + if (entryPos + Constants.DIR_ENTRY_BYTES > searchBuffer.limit()) throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Directory entry exceeds buffer"); - + searchBuffer.position(entryPos); short midFieldId = searchBuffer.getShort(); - + if (midFieldId < fieldId) { low = mid + 1; } else if (midFieldId > fieldId) { @@ -390,36 +384,36 @@ private Directory findDirectoryEntry(int fieldId) throws ImprintException { return deserializeDirectoryEntry(searchBuffer); } } - + return null; } - + private int findEndOffset(int currentFieldId) throws ImprintException { var scanBuffer = directoryBuffer.duplicate().order(ByteOrder.LITTLE_ENDIAN); - + int count = getDirectoryCount(); if (count == 0) return payload.limit(); - + // Advance past varint VarInt.decode(scanBuffer); int directoryStartPos = scanBuffer.position(); - + int low = 0; int high = count - 1; int nextOffset = payload.limit(); - + // Binary search for first field with fieldId > currentFieldId while (low <= high) { int mid = (low + high) >>> 1; int entryPos = directoryStartPos + (mid * Constants.DIR_ENTRY_BYTES); - + if (entryPos + Constants.DIR_ENTRY_BYTES > scanBuffer.limit()) break; - + scanBuffer.position(entryPos); short fieldId = scanBuffer.getShort(); scanBuffer.get(); // skip type int offset = scanBuffer.getInt(); - + if (fieldId > currentFieldId) { nextOffset = offset; high = mid - 1; @@ -427,26 +421,26 @@ private int findEndOffset(int currentFieldId) throws ImprintException { low = mid + 1; } } - + return nextOffset; } - + private Directory deserializeDirectoryEntry(ByteBuffer buffer) throws ImprintException { if (buffer.remaining() < Constants.DIR_ENTRY_BYTES) throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for directory entry"); - + short id = buffer.getShort(); var typeCode = TypeCode.fromByte(buffer.get()); int offset = buffer.getInt(); - + return new Directory.Entry(id, typeCode, offset); } - + /** * DirectoryView */ private class ImprintDirectoryView implements Directory.DirectoryView { - + @Override public Directory findEntry(int fieldId) { try { @@ -469,18 +463,18 @@ public List toList() { } return list; } - + @Override public int size() { return getDirectoryCount(); } - + @Override public Iterator iterator() { return new ImprintDirectoryIterator(); } } - + /** * Iterator that parses directory entries lazily from raw bytes. */ @@ -488,11 +482,11 @@ private class ImprintDirectoryIterator implements Iterator { private final ByteBuffer iterBuffer; private final int totalCount; private int currentIndex; - + ImprintDirectoryIterator() { this.iterBuffer = directoryBuffer.duplicate().order(ByteOrder.LITTLE_ENDIAN); this.totalCount = getDirectoryCount(); - + try { // Skip past varint to first entry VarInt.decode(iterBuffer); @@ -501,18 +495,18 @@ private class ImprintDirectoryIterator implements Iterator { } this.currentIndex = 0; } - + @Override public boolean hasNext() { return currentIndex < totalCount; } - + @Override public Directory next() { if (!hasNext()) { throw new NoSuchElementException(); } - + try { var entry = deserializeDirectoryEntry(iterBuffer); currentIndex++; @@ -522,45 +516,7 @@ public Directory next() { } } } - - /** - * Used by {@link ImprintRecordBuilder} with sorted field data. - * Creates directory buffer from field data and calculated offsets. - * - * @param sortedFields Array of FieldData objects sorted by ID - * @param offsets Array of payload offsets corresponding to each field - * @param fieldCount Number of valid fields to process - */ - static ByteBuffer createDirectoryBufferFromSorted(Object[] sortedFields, int[] offsets, int fieldCount) { - if (fieldCount == 0) - return createEmptyDirectoryBuffer(); - - int size = calculateDirectorySize(fieldCount); - var buffer = ByteBuffer.allocate(size); - buffer.order(ByteOrder.LITTLE_ENDIAN); - VarInt.encode(fieldCount, buffer); - - //this ends up being kind of a hotspot for some reason, probably boundary checking. - //Direct writes might help a bit it could get difficult since pretty much all the other - //frameworks just go straight for Unsafe - for (int i = 0; i < fieldCount; i++) { - var fieldData = (ImprintRecordBuilder.FieldData) sortedFields[i]; - buffer.putShort(fieldData.id); - buffer.put(fieldData.value.getTypeCode().getCode()); - buffer.putInt(offsets[i]); - } - - buffer.flip(); - return buffer; - } - private static ByteBuffer createEmptyDirectoryBuffer() { - ByteBuffer buffer = ByteBuffer.allocate(1); - VarInt.encode(0, buffer); - buffer.flip(); - return buffer; - } - /** * Parse a header from a ByteBuffer without advancing the buffer position. * Utility method shared between {@link ImprintRecord} and {@link ImprintOperations}. @@ -573,14 +529,14 @@ public static Header parseHeaderFromBuffer(ByteBuffer buffer) throws ImprintExce buffer.position(startPos); } } - + /** * Calculate the size needed to store a directory with the given entry count. */ public static int calculateDirectorySize(int entryCount) { return VarInt.encodedLength(entryCount) + (entryCount * Constants.DIR_ENTRY_BYTES); } - + /** * Container for separated directory and payload buffer sections. * Utility class shared between {@link ImprintRecord} and {@link ImprintOperations}. @@ -589,14 +545,14 @@ public static class BufferSections { public final ByteBuffer directoryBuffer; public final ByteBuffer payloadBuffer; public final int directoryCount; - + public BufferSections(ByteBuffer directoryBuffer, ByteBuffer payloadBuffer, int directoryCount) { this.directoryBuffer = directoryBuffer; this.payloadBuffer = payloadBuffer; this.directoryCount = directoryCount; } } - + /** * Extract directory and payload sections from a serialized buffer. * Utility method shared between {@link ImprintRecord} and {@link ImprintOperations}. @@ -604,26 +560,26 @@ public BufferSections(ByteBuffer directoryBuffer, ByteBuffer payloadBuffer, int public static BufferSections extractBufferSections(ByteBuffer buffer, Header header) throws ImprintException { // Skip header buffer.position(buffer.position() + Constants.HEADER_BYTES); - + // Parse directory section int directoryStartPos = buffer.position(); var countResult = VarInt.decode(buffer); int directoryCount = countResult.getValue(); int directorySize = countResult.getBytesRead() + (directoryCount * Constants.DIR_ENTRY_BYTES); - + // Create directory buffer buffer.position(directoryStartPos); var directoryBuffer = buffer.slice(); directoryBuffer.limit(directorySize); - + // Advance to payload buffer.position(buffer.position() + directorySize); var payloadBuffer = buffer.slice(); payloadBuffer.limit(header.getPayloadSize()); - + return new BufferSections(directoryBuffer, payloadBuffer, directoryCount); } - + private static Header parseHeader(ByteBuffer buffer) throws ImprintException { if (buffer.remaining() < Constants.HEADER_BYTES) throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for header"); @@ -635,16 +591,16 @@ private static Header parseHeader(ByteBuffer buffer) throws ImprintException { throw new ImprintException(ErrorType.INVALID_MAGIC, "Invalid magic byte"); if (version != Constants.VERSION) throw new ImprintException(ErrorType.UNSUPPORTED_VERSION, "Unsupported version: " + version); - + var flags = new Flags(buffer.get()); int fieldSpaceId = buffer.getInt(); int schemaHash = buffer.getInt(); int payloadSize = buffer.getInt(); - + return new Header(flags, new SchemaId(fieldSpaceId, schemaHash), payloadSize); } - - private Value deserializeValue(com.imprint.types.TypeCode typeCode, ByteBuffer buffer) throws ImprintException { + + private Object deserializePrimitive(com.imprint.types.TypeCode typeCode, ByteBuffer buffer) throws ImprintException { var valueBuffer = buffer.duplicate().order(ByteOrder.LITTLE_ENDIAN); switch (typeCode) { case NULL: @@ -655,14 +611,81 @@ private Value deserializeValue(com.imprint.types.TypeCode typeCode, ByteBuffer b case FLOAT64: case BYTES: case STRING: + return ImprintDeserializers.deserializePrimitive(valueBuffer, typeCode); case ARRAY: + return deserializePrimitiveArray(valueBuffer); case MAP: - return typeCode.getHandler().deserialize(valueBuffer); + return deserializePrimitiveMap(valueBuffer); case ROW: - var nestedRecord = deserialize(valueBuffer); - return Value.fromRow(nestedRecord); + return deserialize(valueBuffer); default: throw new ImprintException(ErrorType.INVALID_TYPE_CODE, "Unknown type code: " + typeCode); } } + + private java.util.List deserializePrimitiveArray(ByteBuffer buffer) throws ImprintException { + VarInt.DecodeResult lengthResult = VarInt.decode(buffer); + int length = lengthResult.getValue(); + + if (length == 0) { + return java.util.Collections.emptyList(); + } + + if (buffer.remaining() < 1) { + throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for ARRAY element type code."); + } + var elementType = TypeCode.fromByte(buffer.get()); + var elements = new ArrayList<>(length); + + for (int i = 0; i < length; i++) { + Object element; + if (elementType == TypeCode.ARRAY) { + element = deserializePrimitiveArray(buffer); + } else if (elementType == TypeCode.MAP) { + element = deserializePrimitiveMap(buffer); + } else if (elementType == TypeCode.ROW) { + element = deserialize(buffer); + } else { + element = ImprintDeserializers.deserializePrimitive(buffer, elementType); + } + elements.add(element); + } + + return elements; + } + + private java.util.Map deserializePrimitiveMap(ByteBuffer buffer) throws ImprintException { + VarInt.DecodeResult lengthResult = VarInt.decode(buffer); + int length = lengthResult.getValue(); + + if (length == 0) { + return java.util.Collections.emptyMap(); + } + + if (buffer.remaining() < 2) { + throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for MAP key/value type codes."); + } + var keyType = TypeCode.fromByte(buffer.get()); + var valueType = TypeCode.fromByte(buffer.get()); + var map = new java.util.HashMap<>(length); + + for (int i = 0; i < length; i++) { + var keyPrimitive = ImprintDeserializers.deserializePrimitive(buffer, keyType); + + Object valuePrimitive; + if (valueType == TypeCode.ARRAY) { + valuePrimitive = deserializePrimitiveArray(buffer); + } else if (valueType == TypeCode.MAP) { + valuePrimitive = deserializePrimitiveMap(buffer); + } else if (valueType == TypeCode.ROW) { + valuePrimitive = deserialize(buffer); + } else { + valuePrimitive = ImprintDeserializers.deserializePrimitive(buffer, valueType); + } + + map.put(keyPrimitive, valuePrimitive); + } + + return map; + } } \ No newline at end of file diff --git a/src/main/java/com/imprint/core/ImprintRecordBuilder.java b/src/main/java/com/imprint/core/ImprintRecordBuilder.java index 5b7f009..967aac7 100644 --- a/src/main/java/com/imprint/core/ImprintRecordBuilder.java +++ b/src/main/java/com/imprint/core/ImprintRecordBuilder.java @@ -3,10 +3,13 @@ import com.imprint.Constants; import com.imprint.error.ErrorType; import com.imprint.error.ImprintException; +import com.imprint.types.ImprintSerializers; import com.imprint.types.MapKey; -import com.imprint.types.Value; +import com.imprint.types.TypeCode; +import com.imprint.util.VarInt; import lombok.SneakyThrows; +import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.*; @@ -35,91 +38,89 @@ @SuppressWarnings("unused") public final class ImprintRecordBuilder { private final SchemaId schemaId; - private final ImprintFieldObjectMap fields = new ImprintFieldObjectMap<>(); + private final ImprintFieldObjectMap fields; private int estimatedPayloadSize = 0; - static final class FieldData { - final short id; - final Value value; + // Direct primitive storage to avoid Value object creation + @lombok.Value + static class FieldValue { + byte typeCode; + Object value; - FieldData(short id, Value value) { - this.id = id; - this.value = value; - } + // Fast factory methods for primitives + static FieldValue ofInt32(int value) { return new FieldValue(TypeCode.INT32.getCode(), value); } + static FieldValue ofInt64(long value) { return new FieldValue(TypeCode.INT64.getCode(), value); } + static FieldValue ofFloat32(float value) { return new FieldValue(TypeCode.FLOAT32.getCode(), value); } + static FieldValue ofFloat64(double value) { return new FieldValue(TypeCode.FLOAT64.getCode(), value); } + static FieldValue ofBool(boolean value) { return new FieldValue(TypeCode.BOOL.getCode(), value); } + static FieldValue ofString(String value) { return new FieldValue(TypeCode.STRING.getCode(), value); } + 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 ofNull() { return new FieldValue(TypeCode.NULL.getCode(), null); } } - ImprintRecordBuilder(SchemaId schemaId) { + this(schemaId, 16); // Default capacity for typical usage (8-16 fields) + } + + ImprintRecordBuilder(SchemaId schemaId, int expectedFieldCount) { this.schemaId = Objects.requireNonNull(schemaId, "SchemaId cannot be null"); + this.fields = new ImprintFieldObjectMap<>(expectedFieldCount); } - // Primitive types with automatic Value wrapping + public ImprintRecordBuilder field(int id, boolean value) { - return addField(id, Value.fromBoolean(value)); + return addField(id, FieldValue.ofBool(value)); } public ImprintRecordBuilder field(int id, int value) { - return addField(id, Value.fromInt32(value)); + return addField(id, FieldValue.ofInt32(value)); } public ImprintRecordBuilder field(int id, long value) { - return addField(id, Value.fromInt64(value)); + return addField(id, FieldValue.ofInt64(value)); } public ImprintRecordBuilder field(int id, float value) { - return addField(id, Value.fromFloat32(value)); + return addField(id, FieldValue.ofFloat32(value)); } public ImprintRecordBuilder field(int id, double value) { - return addField(id, Value.fromFloat64(value)); + return addField(id, FieldValue.ofFloat64(value)); } public ImprintRecordBuilder field(int id, String value) { - return addField(id, Value.fromString(value)); + return addField(id, FieldValue.ofString(value)); } public ImprintRecordBuilder field(int id, byte[] value) { - return addField(id, Value.fromBytes(value)); + return addField(id, FieldValue.ofBytes(value)); } - // Collections with automatic conversion + // Collections - store as raw collections, convert during serialization public ImprintRecordBuilder field(int id, List values) { - var convertedValues = new ArrayList(values.size()); - for (var item : values) { - convertedValues.add(convertToValue(item)); - } - return addField(id, Value.fromArray(convertedValues)); + return addField(id, FieldValue.ofArray(values)); } public ImprintRecordBuilder field(int id, Map map) { - var convertedMap = new HashMap(map.size()); - for (var entry : map.entrySet()) { - var key = convertToMapKey(entry.getKey()); - var value = convertToValue(entry.getValue()); - convertedMap.put(key, value); - } - return addField(id, Value.fromMap(convertedMap)); + return addField(id, FieldValue.ofMap(map)); } // Nested records public ImprintRecordBuilder field(int id, ImprintRecord nestedRecord) { - return addField(id, Value.fromRow(nestedRecord)); + return addField(id, new FieldValue(TypeCode.ROW.getCode(), nestedRecord)); } // Explicit null field public ImprintRecordBuilder nullField(int id) { - return addField(id, Value.nullValue()); - } - - // Direct Value API (escape hatch for advanced usage) - public ImprintRecordBuilder field(int id, Value value) { - return addField(id, value); + return addField(id, FieldValue.ofNull()); } // Conditional field addition public ImprintRecordBuilder fieldIf(boolean condition, int id, Object value) { if (condition) { - return field(id, convertToValue(value)); + return addField(id, convertToFieldValue(value)); } return this; } @@ -131,7 +132,7 @@ public ImprintRecordBuilder fieldIfNotNull(int id, Object value) { // Bulk operations public ImprintRecordBuilder fields(Map fieldsMap) { for (var entry : fieldsMap.entrySet()) { - field(entry.getKey(), convertToValue(entry.getValue())); + addField(entry.getKey(), convertToFieldValue(entry.getValue())); } return this; } @@ -168,26 +169,33 @@ public ImprintRecord build() throws ImprintException { * @throws ImprintException if serialization fails. */ public ByteBuffer buildToBuffer() throws ImprintException { - // 1. Sort fields by ID for directory ordering (zero allocation) + // 1. Calculate conservative size BEFORE sorting (which invalidates the map) + int conservativeSize = calculateConservativePayloadSize(); + + // 2. Sort fields by ID for directory ordering (zero allocation) var sortedFieldsResult = getSortedFieldsResult(); - var sortedFields = sortedFieldsResult.values; + var sortedValues = sortedFieldsResult.values; + var sortedKeys = sortedFieldsResult.keys; var fieldCount = sortedFieldsResult.count; - - // 2. Serialize payload and calculate offsets - var payloadBuffer = ByteBuffer.allocate(estimatePayloadSize()); - payloadBuffer.order(ByteOrder.LITTLE_ENDIAN); - int[] offsets = new int[fieldCount]; - for (int i = 0; i < fieldCount; i++) { - var fieldData = (FieldData) sortedFields[i]; - offsets[i] = payloadBuffer.position(); - serializeValue(fieldData.value, payloadBuffer); + // 3. Serialize payload and calculate offsets with overflow handling + PayloadSerializationResult result = null; + int bufferSizeMultiplier = 1; + + while (result == null && bufferSizeMultiplier <= 64) { + try { + result = serializePayload(sortedValues, fieldCount, conservativeSize, bufferSizeMultiplier); + } catch (BufferOverflowException e) { + bufferSizeMultiplier *= 2; // Try 2x, 4x, 8x, 16x, 32x, 64x + } + } + + if (result == null) { + throw new ImprintException(ErrorType.SERIALIZATION_ERROR, + "Failed to serialize payload even with 64x buffer size"); } - payloadBuffer.flip(); - var payloadView = payloadBuffer.slice().asReadOnlyBuffer(); - // 3. Create directory buffer and serialize to final buffer - return serializeToBuffer(schemaId, sortedFields, offsets, fieldCount, payloadView); + return serializeToBuffer(schemaId, sortedKeys, sortedValues, result.offsets, fieldCount, result.payload); } /** @@ -195,78 +203,58 @@ public ByteBuffer buildToBuffer() throws ImprintException { * If a field with the given ID already exists, it will be replaced. * * @param id the field ID - * @param value the field value (cannot be null - use nullField() for explicit nulls) + * @param fieldValue the field value with type code * @return this builder for method chaining */ - private ImprintRecordBuilder addField(int id, Value value) { - Objects.requireNonNull(value, "Value cannot be null - use nullField() for explicit null values"); - var newEntry = new FieldData((short) id, value); + @SneakyThrows + private ImprintRecordBuilder addField(int id, FieldValue fieldValue) { + Objects.requireNonNull(fieldValue, "FieldValue cannot be null"); + int newSize = estimateFieldSize(fieldValue); + var oldEntry = fields.putAndReturnOld(id, fieldValue); - // Check if replacing an existing field - var oldEntry = fields.get(id); if (oldEntry != null) { - estimatedPayloadSize -= estimateValueSize(oldEntry.value); + int oldSize = estimateFieldSize(oldEntry); + estimatedPayloadSize += newSize - oldSize; + } else { + estimatedPayloadSize += newSize; } - // Add or replace field - fields.put(id, newEntry); - estimatedPayloadSize += estimateValueSize(newEntry.value); return this; } - private Value convertToValue(Object obj) { + private FieldValue convertToFieldValue(Object obj) { if (obj == null) { - return Value.nullValue(); - } - - if (obj instanceof Value) { - return (Value) obj; + return FieldValue.ofNull(); } - - // Auto-boxing conversion if (obj instanceof Boolean) { - return Value.fromBoolean((Boolean) obj); + return FieldValue.ofBool((Boolean) obj); } if (obj instanceof Integer) { - return Value.fromInt32((Integer) obj); + return FieldValue.ofInt32((Integer) obj); } if (obj instanceof Long) { - return Value.fromInt64((Long) obj); + return FieldValue.ofInt64((Long) obj); } if (obj instanceof Float) { - return Value.fromFloat32((Float) obj); + return FieldValue.ofFloat32((Float) obj); } if (obj instanceof Double) { - return Value.fromFloat64((Double) obj); + return FieldValue.ofFloat64((Double) obj); } if (obj instanceof String) { - return Value.fromString((String) obj); + return FieldValue.ofString((String) obj); } if (obj instanceof byte[]) { - return Value.fromBytes((byte[]) obj); + return FieldValue.ofBytes((byte[]) obj); } if (obj instanceof List) { - @SuppressWarnings("unchecked") - List list = (List) obj; - var convertedValues = new ArrayList(list.size()); - for (var item : list) { - convertedValues.add(convertToValue(item)); - } - return Value.fromArray(convertedValues); + return FieldValue.ofArray((List) obj); } if (obj instanceof Map) { - @SuppressWarnings("unchecked") - Map map = (Map) obj; - var convertedMap = new HashMap(map.size()); - for (var entry : map.entrySet()) { - var key = convertToMapKey(entry.getKey()); - var value = convertToValue(entry.getValue()); - convertedMap.put(key, value); - } - return Value.fromMap(convertedMap); + return FieldValue.ofMap((Map) obj); } if (obj instanceof ImprintRecord) { - return Value.fromRow((ImprintRecord) obj); + return new FieldValue(TypeCode.ROW.getCode(), obj); } throw new IllegalArgumentException("Unsupported type for auto-conversion: " + obj.getClass().getName()); @@ -289,65 +277,123 @@ private MapKey convertToMapKey(Object obj) { throw new IllegalArgumentException("Unsupported map key type: " + obj.getClass().getName()); } - private int estimatePayloadSize() { - // Add 25% buffer to reduce reallocations and handle VarInt encoding fluctuations. - return Math.max(estimatedPayloadSize + (estimatedPayloadSize / 4), fields.size() * 16); + private int estimateFieldSize(FieldValue fieldValue) { + TypeCode typeCode; + try { + typeCode = TypeCode.fromByte(fieldValue.typeCode); + } catch (ImprintException e) { + throw new RuntimeException("Invalid type code in FieldValue: " + fieldValue.typeCode, e); + } + return ImprintSerializers.estimateSize(typeCode, fieldValue.value); } - /** - * Estimates the serialized size in bytes for a given value. - * - * @param value the value to estimate size for - * @return estimated size in bytes including type-specific overhead - */ - @SneakyThrows - private int estimateValueSize(Value value) { - // Use TypeHandler for simple types - switch (value.getTypeCode()) { - case NULL: - case BOOL: - case INT32: - case INT64: - case FLOAT32: - case FLOAT64: - case BYTES: - case STRING: - case ARRAY: - case MAP: - return value.getTypeCode().getHandler().estimateSize(value); + private int calculateConservativePayloadSize() { + // Add 25% buffer for safety margin + return Math.max(estimatedPayloadSize + (estimatedPayloadSize / 4), 4096); + } - case ROW: - Value.RowValue rowValue = (Value.RowValue) value; - return rowValue.getValue().estimateSerializedSize(); + private static class PayloadSerializationResult { + final int[] offsets; + final ByteBuffer payload; - default: - throw new ImprintException(ErrorType.SERIALIZATION_ERROR, "Unknown type code: " + value.getTypeCode()); + PayloadSerializationResult(int[] offsets, ByteBuffer payload) { + this.offsets = offsets; + this.payload = payload; } } - private void serializeValue(Value value, ByteBuffer buffer) throws ImprintException { - switch (value.getTypeCode()) { + private PayloadSerializationResult serializePayload(Object[] sortedFields, int fieldCount, int conservativeSize, int sizeMultiplier) throws ImprintException { + var payloadBuffer = ByteBuffer.allocate(conservativeSize * sizeMultiplier); + payloadBuffer.order(ByteOrder.LITTLE_ENDIAN); + return doSerializePayload(sortedFields, fieldCount, payloadBuffer); + } + + private PayloadSerializationResult doSerializePayload(Object[] sortedFields, int fieldCount, ByteBuffer payloadBuffer) throws ImprintException { + int[] offsets = new int[fieldCount]; + for (int i = 0; i < fieldCount; i++) { + var fieldValue = (FieldValue) sortedFields[i]; + offsets[i] = payloadBuffer.position(); + serializeFieldValue(fieldValue, payloadBuffer); + } + payloadBuffer.flip(); + var payloadView = payloadBuffer.slice().asReadOnlyBuffer(); + return new PayloadSerializationResult(offsets, payloadView); + } + + private void serializeFieldValue(FieldValue fieldValue, ByteBuffer buffer) throws ImprintException { + var typeCode = TypeCode.fromByte(fieldValue.typeCode); + var value = fieldValue.value; + switch (typeCode) { case NULL: + ImprintSerializers.serializeNull(buffer); + break; case BOOL: + ImprintSerializers.serializeBool((Boolean) value, buffer); + break; case INT32: + ImprintSerializers.serializeInt32((Integer) value, buffer); + break; case INT64: + ImprintSerializers.serializeInt64((Long) value, buffer); + break; case FLOAT32: + ImprintSerializers.serializeFloat32((Float) value, buffer); + break; case FLOAT64: - case BYTES: + ImprintSerializers.serializeFloat64((Double) value, buffer); + break; case STRING: + ImprintSerializers.serializeString((String) value, buffer); + break; + case BYTES: + ImprintSerializers.serializeBytes((byte[]) value, buffer); + break; case ARRAY: + serializeArray((List) value, buffer); + break; case MAP: - value.getTypeCode().getHandler().serialize(value, buffer); + serializeMap((Map) value, buffer); break; - //TODO eliminate this switch entirely by implementing a ROW TypeHandler case ROW: - Value.RowValue rowValue = (Value.RowValue) value; - var serializedRow = rowValue.getValue().serializeToBuffer(); + // Nested records + var nestedRecord = (ImprintRecord) value; + var serializedRow = nestedRecord.serializeToBuffer(); buffer.put(serializedRow); break; - default: - throw new ImprintException(ErrorType.SERIALIZATION_ERROR, "Unknown type code: " + value.getTypeCode()); + throw new ImprintException(ErrorType.SERIALIZATION_ERROR, "Unknown type code: " + typeCode); + } + } + + //TODO arrays and maps need to be handled better + private void serializeArray(List list, ByteBuffer buffer) throws ImprintException { + ImprintSerializers.serializeArray(list, buffer, + this::getTypeCodeForObject, + this::serializeObjectDirect); + } + + private void serializeMap(Map map, ByteBuffer buffer) throws ImprintException { + ImprintSerializers.serializeMap(map, buffer, + this::convertToMapKey, + this::getTypeCodeForObject, + this::serializeObjectDirect); + } + + private TypeCode getTypeCodeForObject(Object obj) { + var fieldValue = convertToFieldValue(obj); + try { + return TypeCode.fromByte(fieldValue.typeCode); + } catch (ImprintException e) { + throw new RuntimeException("Invalid type code", e); + } + } + + private void serializeObjectDirect(Object obj, ByteBuffer buffer) { + try { + var fieldValue = convertToFieldValue(obj); + serializeFieldValue(fieldValue, buffer); + } catch (ImprintException e) { + throw new RuntimeException("Serialization failed", e); } } @@ -355,18 +401,18 @@ private void serializeValue(Value value, ByteBuffer buffer) throws ImprintExcept * Get fields sorted by ID from the map. * Returns internal map array reference + count to avoid any copying but sacrifices the map structure in the process. */ - private ImprintFieldObjectMap.SortedValuesResult getSortedFieldsResult() { - return fields.getSortedValues(); + private ImprintFieldObjectMap.SortedFieldsResult getSortedFieldsResult() { + return fields.getSortedFields(); } /** * Serialize components into a single ByteBuffer. */ - private static ByteBuffer serializeToBuffer(SchemaId schemaId, Object[] sortedFields, int[] offsets, int fieldCount, ByteBuffer payload) { + private static ByteBuffer serializeToBuffer(SchemaId schemaId, short[] sortedKeys, Object[] sortedValues, int[] offsets, int fieldCount, ByteBuffer payload) { var header = new Header(new Flags((byte) 0), schemaId, payload.remaining()); - var directoryBuffer = ImprintRecord.createDirectoryBufferFromSorted(sortedFields, offsets, fieldCount); + int directorySize = ImprintRecord.calculateDirectorySize(fieldCount); - int finalSize = Constants.HEADER_BYTES + directoryBuffer.remaining() + payload.remaining(); + int finalSize = Constants.HEADER_BYTES + directorySize + payload.remaining(); var finalBuffer = ByteBuffer.allocate(finalSize); finalBuffer.order(ByteOrder.LITTLE_ENDIAN); @@ -377,10 +423,44 @@ private static ByteBuffer serializeToBuffer(SchemaId schemaId, Object[] sortedFi finalBuffer.putInt(header.getSchemaId().getFieldSpaceId()); finalBuffer.putInt(header.getSchemaId().getSchemaHash()); finalBuffer.putInt(header.getPayloadSize()); - finalBuffer.put(directoryBuffer); + + // Write directory with FieldValue type codes + writeDirectoryToBuffer(sortedKeys, sortedValues, offsets, fieldCount, finalBuffer); + + // Write payload finalBuffer.put(payload); finalBuffer.flip(); return finalBuffer.asReadOnlyBuffer(); } -} \ No newline at end of file + + /** + * Write directory entries directly to buffer for FieldValue objects. + */ + private static void writeDirectoryToBuffer(short[] sortedKeys, Object[] sortedValues, int[] offsets, int fieldCount, ByteBuffer buffer) { + // Write field count at the beginning of directory + // Optimize VarInt encoding for common case (< 128 fields = single byte) + if (fieldCount < 128) { + buffer.put((byte) fieldCount); + } else { + VarInt.encode(fieldCount, buffer); + } + + // Early return for empty directory + if (fieldCount == 0) + return; + + + //hopefully JIT vectorizes this + for (int i = 0; i < fieldCount; i++) { + var fieldValue = (FieldValue) sortedValues[i]; + int pos = buffer.position(); + buffer.putShort(pos, sortedKeys[i]); // bytes 0-1: field ID + buffer.put(pos + 2, fieldValue.typeCode); // byte 2: type code + buffer.putInt(pos + 3, offsets[i]); // bytes 3-6: offset + // Advance buffer position by 7 bytes + buffer.position(pos + 7); + } + } + +} diff --git a/src/main/java/com/imprint/ops/ImprintOperations.java b/src/main/java/com/imprint/ops/ImprintOperations.java index 52ec5a0..54e594a 100644 --- a/src/main/java/com/imprint/ops/ImprintOperations.java +++ b/src/main/java/com/imprint/ops/ImprintOperations.java @@ -76,8 +76,8 @@ private static ByteBuffer mergeRawSections(Header firstHeader, ImprintRecord.Buf int totalMergedPayloadSize = 0; int currentMergedOffset = 0; - RawDirectoryEntry firstEntry = firstDirIter.hasNext() ? firstDirIter.next() : null; - RawDirectoryEntry secondEntry = secondDirIter.hasNext() ? secondDirIter.next() : null; + var firstEntry = firstDirIter.hasNext() ? firstDirIter.next() : null; + var secondEntry = secondDirIter.hasNext() ? secondDirIter.next() : null; // Merge directories and collect payload chunks while (firstEntry != null || secondEntry != null) { @@ -90,9 +90,9 @@ private static ByteBuffer mergeRawSections(Header firstHeader, ImprintRecord.Buf sourcePayload = getFieldPayload(firstSections.payloadBuffer, firstEntry, firstDirIter); // Skip duplicate in second if present - if (secondEntry != null && firstEntry.fieldId == secondEntry.fieldId) { + if (secondEntry != null && firstEntry.fieldId == secondEntry.fieldId) secondEntry = secondDirIter.hasNext() ? secondDirIter.next() : null; - } + firstEntry = firstDirIter.hasNext() ? firstDirIter.next() : null; } else { // Take from second diff --git a/src/main/java/com/imprint/types/ImprintDeserializers.java b/src/main/java/com/imprint/types/ImprintDeserializers.java new file mode 100644 index 0000000..18f561d --- /dev/null +++ b/src/main/java/com/imprint/types/ImprintDeserializers.java @@ -0,0 +1,76 @@ +package com.imprint.types; + +import com.imprint.error.ErrorType; +import com.imprint.error.ImprintException; +import com.imprint.util.VarInt; +import lombok.experimental.UtilityClass; + +import java.nio.ByteBuffer; + +/** + * Static primitive deserialization methods for all Imprint types. + * Returns native Java objects instead of Value wrappers for better performance. + */ +@UtilityClass +public final class ImprintDeserializers { + + // Primitive boxed deserializers + public static Object deserializePrimitive(ByteBuffer buffer, TypeCode typeCode) throws ImprintException { + switch (typeCode) { + case NULL: + return null; + case BOOL: + if (buffer.remaining() < 1) { + throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for bool"); + } + byte boolByte = buffer.get(); + if (boolByte == 0) return false; + if (boolByte == 1) return true; + throw new ImprintException(ErrorType.SCHEMA_ERROR, "Invalid boolean value: " + boolByte); + case INT32: + if (buffer.remaining() < 4) { + throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for int32"); + } + return buffer.getInt(); + case INT64: + if (buffer.remaining() < 8) { + throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for int64"); + } + return buffer.getLong(); + case FLOAT32: + if (buffer.remaining() < 4) { + throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for float32"); + } + return buffer.getFloat(); + case FLOAT64: + if (buffer.remaining() < 8) { + throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for float64"); + } + return buffer.getDouble(); + case BYTES: + VarInt.DecodeResult lengthResult = VarInt.decode(buffer); + int length = lengthResult.getValue(); + if (buffer.remaining() < length) { + throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, + "Not enough bytes for bytes value data after VarInt. Needed: " + + length + ", available: " + buffer.remaining()); + } + byte[] bytes = new byte[length]; + buffer.get(bytes); + return bytes; + case STRING: + VarInt.DecodeResult strLengthResult = VarInt.decode(buffer); + int strLength = strLengthResult.getValue(); + if (buffer.remaining() < strLength) { + 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); + default: + throw new ImprintException(ErrorType.SERIALIZATION_ERROR, "Cannot deserialize " + typeCode + " as primitive"); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/imprint/types/ImprintSerializers.java b/src/main/java/com/imprint/types/ImprintSerializers.java new file mode 100644 index 0000000..f76f444 --- /dev/null +++ b/src/main/java/com/imprint/types/ImprintSerializers.java @@ -0,0 +1,172 @@ +package com.imprint.types; + +import com.imprint.error.ErrorType; +import com.imprint.error.ImprintException; +import com.imprint.util.VarInt; +import lombok.experimental.UtilityClass; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +/** + * Static serialization methods for all Imprint types. + * Eliminates virtual dispatch overhead from TypeHandler interface. + */ +@UtilityClass +public final class ImprintSerializers { + + + // Primitive serializers + public static void serializeBool(boolean value, ByteBuffer buffer) { + buffer.put((byte) (value ? 1 : 0)); + } + + public static void serializeInt32(int value, ByteBuffer buffer) { + buffer.putInt(value); + } + + public static void serializeInt64(long value, ByteBuffer buffer) { + buffer.putLong(value); + } + + public static void serializeFloat32(float value, ByteBuffer buffer) { + buffer.putFloat(value); + } + + public static void serializeFloat64(double value, ByteBuffer buffer) { + buffer.putDouble(value); + } + + public static void serializeString(String value, ByteBuffer buffer) { + byte[] utf8Bytes = value.getBytes(StandardCharsets.UTF_8); + VarInt.encode(utf8Bytes.length, buffer); + buffer.put(utf8Bytes); + } + + public static void serializeBytes(byte[] value, ByteBuffer buffer) { + VarInt.encode(value.length, buffer); + buffer.put(value); + } + + public static void serializeArray(java.util.List list, ByteBuffer buffer, + java.util.function.Function typeConverter, + java.util.function.BiConsumer elementSerializer) throws ImprintException { + VarInt.encode(list.size(), buffer); + + if (list.isEmpty()) return; // Empty arrays don't need type code + + // Convert first element to determine element type + Object firstElement = list.get(0); + TypeCode firstTypeCode = typeConverter.apply(firstElement); + buffer.put(firstTypeCode.getCode()); + + // Serialize all elements - they must be same type + for (Object element : list) { + TypeCode elementTypeCode = typeConverter.apply(element); + if (elementTypeCode != firstTypeCode) { + throw new ImprintException(ErrorType.SCHEMA_ERROR, + "Array elements must have same type"); + } + elementSerializer.accept(element, buffer); + } + } + + public static void serializeMap(java.util.Map map, ByteBuffer buffer, + java.util.function.Function keyConverter, + java.util.function.Function typeConverter, + java.util.function.BiConsumer valueSerializer) throws ImprintException { + VarInt.encode(map.size(), buffer); + + if (map.isEmpty()) return; + + var iterator = map.entrySet().iterator(); + var first = iterator.next(); + + // Convert key and value to determine types + MapKey firstKey = keyConverter.apply(first.getKey()); + TypeCode firstValueType = typeConverter.apply(first.getValue()); + + buffer.put(firstKey.getTypeCode().getCode()); + buffer.put(firstValueType.getCode()); + + // Serialize first pair + serializeMapKeyDirect(firstKey, buffer); + valueSerializer.accept(first.getValue(), buffer); + + // Serialize remaining pairs + while (iterator.hasNext()) { + var entry = iterator.next(); + MapKey key = keyConverter.apply(entry.getKey()); + TypeCode valueType = typeConverter.apply(entry.getValue()); + + if (key.getTypeCode() != firstKey.getTypeCode()) { + throw new ImprintException(ErrorType.SCHEMA_ERROR, + "Map keys must have same type"); + } + if (valueType != firstValueType) { + throw new ImprintException(ErrorType.SCHEMA_ERROR, + "Map values must have same type"); + } + + serializeMapKeyDirect(key, buffer); + valueSerializer.accept(entry.getValue(), buffer); + } + } + + private static void serializeMapKeyDirect(MapKey key, ByteBuffer buffer) throws ImprintException { + switch (key.getTypeCode()) { + case INT32: + buffer.putInt(((MapKey.Int32Key) key).getValue()); + break; + case INT64: + buffer.putLong(((MapKey.Int64Key) key).getValue()); + break; + case BYTES: + byte[] bytes = ((MapKey.BytesKey) key).getValue(); + VarInt.encode(bytes.length, buffer); + buffer.put(bytes); + break; + case STRING: + String str = ((MapKey.StringKey) key).getValue(); + byte[] stringBytes = str.getBytes(StandardCharsets.UTF_8); + VarInt.encode(stringBytes.length, buffer); + buffer.put(stringBytes); + break; + default: + throw new ImprintException(ErrorType.SERIALIZATION_ERROR, + "Invalid map key type: " + key.getTypeCode()); + } + } + + @SuppressWarnings("unused") + public static void serializeNull(ByteBuffer buffer) { + // NULL values have no payload data but the method helps intent + } + + // Rough size estimate since actual takes time; might be able to accomodate this better with a growable buffer though + public static int estimateSize(TypeCode typeCode, Object value) { + switch (typeCode) { + case NULL: return 0; + case BOOL: return 1; + case INT32: + case FLOAT32: + return 4; + case INT64: + case FLOAT64: + return 8; + case STRING: + String str = (String) value; + return str.length() > 1000 ? 5 + str.length() * 3 : 256; + case BYTES: + byte[] bytes = (byte[]) value; + return bytes.length > 1000 ? 5 + bytes.length : 256; + case ARRAY: + case MAP: + return 512; //just rough estimate/guess for now; + case ROW: + return 1024; + default: + return 64; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/imprint/types/MapKey.java b/src/main/java/com/imprint/types/MapKey.java index c0e0747..5961f4b 100644 --- a/src/main/java/com/imprint/types/MapKey.java +++ b/src/main/java/com/imprint/types/MapKey.java @@ -35,45 +35,44 @@ public static MapKey fromString(String value) { return new StringKey(value); } - public static MapKey fromValue(Value value) throws ImprintException { - switch (value.getTypeCode()) { + /** + * Create MapKey from primitive object and type code (optimized, no Value objects). + */ + public static MapKey fromPrimitive(TypeCode typeCode, Object primitiveValue) throws ImprintException { + switch (typeCode) { case INT32: - return fromInt32(((Value.Int32Value) value).getValue()); + return fromInt32((Integer) primitiveValue); case INT64: - return fromInt64(((Value.Int64Value) value).getValue()); + return fromInt64((Long) primitiveValue); case BYTES: - if (value instanceof Value.BytesBufferValue) { - return fromBytes(((Value.BytesBufferValue) value).getValue()); - } else { - return fromBytes(((Value.BytesValue) value).getValue()); - } + return fromBytes((byte[]) primitiveValue); case STRING: - if (value instanceof Value.StringBufferValue) { - return fromString(((Value.StringBufferValue) value).getValue()); - } else { - return fromString(((Value.StringValue) value).getValue()); - } + return fromString((String) primitiveValue); default: - throw new ImprintException(ErrorType.TYPE_MISMATCH, - "Cannot convert " + value.getTypeCode() + " to MapKey"); + throw new ImprintException(ErrorType.TYPE_MISMATCH, "Cannot convert " + typeCode + " to MapKey"); } } - public Value toValue() { + + /** + * Get the primitive value as Object (optimized, no Value objects). + */ + public Object getPrimitiveValue() { switch (getTypeCode()) { case INT32: - return Value.fromInt32(((Int32Key) this).getValue()); + return ((Int32Key) this).getValue(); case INT64: - return Value.fromInt64(((Int64Key) this).getValue()); + return ((Int64Key) this).getValue(); case BYTES: - return Value.fromBytes(((BytesKey) this).getValue()); + return ((BytesKey) this).getValue(); case STRING: - return Value.fromString(((StringKey) this).getValue()); + return ((StringKey) this).getValue(); default: throw new IllegalStateException("Unknown MapKey type: " + getTypeCode()); } } + @Getter @EqualsAndHashCode(callSuper = false) public static class Int32Key extends MapKey { diff --git a/src/main/java/com/imprint/types/TypeCode.java b/src/main/java/com/imprint/types/TypeCode.java index 3447f8b..7c80d87 100644 --- a/src/main/java/com/imprint/types/TypeCode.java +++ b/src/main/java/com/imprint/types/TypeCode.java @@ -7,22 +7,21 @@ /** * Type codes for Imprint values. */ +@Getter public enum TypeCode { - NULL(0x0, TypeHandler.NULL), - BOOL(0x1, TypeHandler.BOOL), - INT32(0x2, TypeHandler.INT32), - INT64(0x3, TypeHandler.INT64), - FLOAT32(0x4, TypeHandler.FLOAT32), - FLOAT64(0x5, TypeHandler.FLOAT64), - BYTES(0x6, TypeHandler.BYTES), - STRING(0x7, TypeHandler.STRING), - ARRAY(0x8, TypeHandler.ARRAY), - MAP(0x9, TypeHandler.MAP), - ROW(0xA, null); // TODO: implement (basically a placeholder for user-defined type) + NULL(0x0), + BOOL(0x1), + INT32(0x2), + INT64(0x3), + FLOAT32(0x4), + FLOAT64(0x5), + BYTES(0x6), + STRING(0x7), + ARRAY(0x8), + MAP(0x9), + ROW(0xA); // TODO: implement (basically a placeholder for user-defined type) - @Getter private final byte code; - private final TypeHandler handler; private static final TypeCode[] LOOKUP = new TypeCode[11]; @@ -32,16 +31,8 @@ public enum TypeCode { } } - TypeCode(int code, TypeHandler handler) { + TypeCode(int code) { this.code = (byte) code; - this.handler = handler; - } - - public TypeHandler getHandler() { - if (handler == null) { - throw new UnsupportedOperationException("Handler not implemented for " + this); - } - return handler; } public static TypeCode fromByte(byte code) throws ImprintException { @@ -49,7 +40,6 @@ public static TypeCode fromByte(byte code) throws ImprintException { var type = LOOKUP[code]; if (type != null) return type; } - throw new ImprintException(ErrorType.INVALID_TYPE_CODE, - "Unknown type code: 0x" + Integer.toHexString(code & 0xFF)); + throw new ImprintException(ErrorType.INVALID_TYPE_CODE, "Unknown type code: 0x" + Integer.toHexString(code & 0xFF)); } } \ No newline at end of file diff --git a/src/main/java/com/imprint/types/TypeHandler.java b/src/main/java/com/imprint/types/TypeHandler.java deleted file mode 100644 index dbc875f..0000000 --- a/src/main/java/com/imprint/types/TypeHandler.java +++ /dev/null @@ -1,442 +0,0 @@ -package com.imprint.types; - -import com.imprint.error.ErrorType; -import com.imprint.error.ImprintException; -import com.imprint.util.VarInt; - -import java.nio.ByteBuffer; -import java.util.*; - -/** - * Interface for handling type-specific serialization, deserialization, and size estimation. - * Note that primitives are basically boxed here which could impact performance slightly - * but having all the types in their own implementation helps keep things organized for now, especially - * for dealing with and testing more complex types in the future. - */ -public interface TypeHandler { - Value deserialize(ByteBuffer buffer) throws ImprintException; - void serialize(Value value, ByteBuffer buffer) throws ImprintException; - int estimateSize(Value value) throws ImprintException; - - // Static implementations for each type - TypeHandler NULL = new TypeHandler() { - @Override - public Value deserialize(ByteBuffer buffer) { - return Value.nullValue(); - } - - @Override - public void serialize(Value value, ByteBuffer buffer) { - // NULL values have no payload - } - - @Override - public int estimateSize(Value value) { - return 0; - } - }; - - TypeHandler BOOL = new TypeHandler() { - @Override - public Value deserialize(ByteBuffer buffer) throws ImprintException { - if (buffer.remaining() < 1) { - throw new ImprintException(com.imprint.error.ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for bool"); - } - byte boolByte = buffer.get(); - if (boolByte == 0) return Value.fromBoolean(false); - if (boolByte == 1) return Value.fromBoolean(true); - throw new ImprintException(com.imprint.error.ErrorType.SCHEMA_ERROR, "Invalid boolean value: " + boolByte); - } - - @Override - public void serialize(Value value, ByteBuffer buffer) { - var boolValue = (Value.BoolValue) value; - buffer.put((byte) (boolValue.getValue() ? 1 : 0)); - } - - @Override - public int estimateSize(Value value) { - return 1; - } - }; - - TypeHandler INT32 = new TypeHandler() { - @Override - public Value deserialize(ByteBuffer buffer) throws ImprintException { - if (buffer.remaining() < 4) { - throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for int32"); - } - return Value.fromInt32(buffer.getInt()); - } - - @Override - public void serialize(Value value, ByteBuffer buffer) { - var int32Value = (Value.Int32Value) value; - buffer.putInt(int32Value.getValue()); - } - - @Override - public int estimateSize(Value value) { - return 4; - } - }; - - TypeHandler INT64 = new TypeHandler() { - @Override - public Value deserialize(ByteBuffer buffer) throws ImprintException { - if (buffer.remaining() < 8) { - throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for int64"); - } - return Value.fromInt64(buffer.getLong()); - } - - @Override - public void serialize(Value value, ByteBuffer buffer) { - Value.Int64Value int64Value = (Value.Int64Value) value; - buffer.putLong(int64Value.getValue()); - } - - @Override - public int estimateSize(Value value) { - return 8; - } - }; - - TypeHandler FLOAT32 = new TypeHandler() { - @Override - public Value deserialize(ByteBuffer buffer) throws ImprintException { - if (buffer.remaining() < 4) { - throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for float32"); - } - return Value.fromFloat32(buffer.getFloat()); - } - - @Override - public void serialize(Value value, ByteBuffer buffer) { - var float32Value = (Value.Float32Value) value; - buffer.putFloat(float32Value.getValue()); - } - - @Override - public int estimateSize(Value value) { - return 4; - } - }; - - TypeHandler FLOAT64 = new TypeHandler() { - @Override - public Value deserialize(ByteBuffer buffer) throws ImprintException { - if (buffer.remaining() < 8) { - throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for float64"); - } - return Value.fromFloat64(buffer.getDouble()); - } - - @Override - public void serialize(Value value, ByteBuffer buffer) { - var float64Value = (Value.Float64Value) value; - buffer.putDouble(float64Value.getValue()); - } - - @Override - public int estimateSize(Value value) { - return 8; - } - }; - - TypeHandler BYTES = new TypeHandler() { - @Override - public Value deserialize(ByteBuffer buffer) throws ImprintException { - VarInt.DecodeResult lengthResult = VarInt.decode(buffer); - int length = lengthResult.getValue(); - if (buffer.remaining() < length) { - throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for bytes value data after VarInt. Slice from readValueBytes is too short. Needed: " + length + ", available: " + buffer.remaining()); - } - var bytesView = buffer.slice(); - bytesView.limit(length); - buffer.position(buffer.position() + length); - return Value.fromBytesBuffer(bytesView.asReadOnlyBuffer()); - } - - @Override - public void serialize(Value value, ByteBuffer buffer) { - if (value instanceof Value.BytesBufferValue) { - Value.BytesBufferValue bufferValue = (Value.BytesBufferValue) value; - var bytesBuffer = bufferValue.getBuffer(); - VarInt.encode(bytesBuffer.remaining(), buffer); - buffer.put(bytesBuffer); - } else { - Value.BytesValue bytesValue = (Value.BytesValue) value; - byte[] bytes = bytesValue.getValue(); - VarInt.encode(bytes.length, buffer); - buffer.put(bytes); - } - } - - @Override - public int estimateSize(Value value) { - if (value instanceof Value.BytesBufferValue) { - Value.BytesBufferValue bufferValue = (Value.BytesBufferValue) value; - int length = bufferValue.getBuffer().remaining(); - return VarInt.encodedLength(length) + length; - } else { - byte[] bytes = ((Value.BytesValue) value).getValue(); - return VarInt.encodedLength(bytes.length) + bytes.length; - } - } - }; - - TypeHandler STRING = new TypeHandler() { - @Override - public Value deserialize(ByteBuffer buffer) throws ImprintException { - VarInt.DecodeResult strLengthResult = VarInt.decode(buffer); - int strLength = strLengthResult.getValue(); - if (buffer.remaining() < strLength) { - throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for string value data after VarInt. Slice from readValueBytes is too short. Needed: " + strLength + ", available: " + buffer.remaining()); - } - var stringBytesView = buffer.slice(); - stringBytesView.limit(strLength); - buffer.position(buffer.position() + strLength); - try { - return Value.fromStringBuffer(stringBytesView); - } catch (Exception e) { - throw new ImprintException(ErrorType.INVALID_UTF8_STRING, "Invalid UTF-8 string or buffer issue: " + e.getMessage()); - } - } - - @Override - public void serialize(Value value, ByteBuffer buffer) { - if (value instanceof Value.StringBufferValue) { - var bufferValue = (Value.StringBufferValue) value; - var stringBuffer = bufferValue.getBuffer(); - VarInt.encode(stringBuffer.remaining(), buffer); - buffer.put(stringBuffer); - } else { - var stringValue = (Value.StringValue) value; - byte[] stringBytes = stringValue.getUtf8Bytes(); - VarInt.encode(stringBytes.length, buffer); - buffer.put(stringBytes); - } - } - - @Override - public int estimateSize(Value value) { - if (value instanceof Value.StringBufferValue) { - Value.StringBufferValue bufferValue = (Value.StringBufferValue) value; - int length = bufferValue.getBuffer().remaining(); - return VarInt.encodedLength(length) + length; - } else { - Value.StringValue stringValue = (Value.StringValue) value; - int utf8Length = stringValue.getUtf8Length(); // Uses cached bytes - return VarInt.encodedLength(utf8Length) + utf8Length; - } - } - }; - - TypeHandler ARRAY = new TypeHandler() { - @Override - public Value deserialize(ByteBuffer buffer) throws ImprintException { - VarInt.DecodeResult lengthResult = VarInt.decode(buffer); - int length = lengthResult.getValue(); - - if (length == 0) { - return Value.fromArray(Collections.emptyList()); - } - - if (buffer.remaining() < 1) { - throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for ARRAY element type code."); - } - var elementType = TypeCode.fromByte(buffer.get()); - var elements = new ArrayList(length); - var elementHandler = elementType.getHandler(); - - //Let each element handler consume what it needs from the buffer - for (int i = 0; i < length; i++) { - var element = elementHandler.deserialize(buffer); //Handler advances buffer position - elements.add(element); - } - - return Value.fromArray(elements); - } - - @Override - public void serialize(Value value, ByteBuffer buffer) throws ImprintException { - var arrayValue = (Value.ArrayValue) value; - var elements = arrayValue.getValue(); - VarInt.encode(elements.size(), buffer); - - if (elements.isEmpty()) return; - - var elementType = elements.get(0).getTypeCode(); - buffer.put(elementType.getCode()); - var elementHandler = elementType.getHandler(); - for (var element : elements) { - if (element.getTypeCode() != elementType) { - throw new ImprintException(ErrorType.SCHEMA_ERROR, - "Array elements must have same type code: " + - element.getTypeCode() + " != " + elementType); - } - elementHandler.serialize(element, buffer); - } - } - - @Override - public int estimateSize(Value value) throws ImprintException { - var arrayValue = (Value.ArrayValue) value; - var elements = arrayValue.getValue(); - int sizeOfLength = VarInt.encodedLength(elements.size()); - if (elements.isEmpty()) { - return sizeOfLength; - } - int sizeOfElementTypeCode = 1; - int arraySize = sizeOfLength + sizeOfElementTypeCode; - var elementHandler = elements.get(0).getTypeCode().getHandler(); - for (var element : elements) { - arraySize += elementHandler.estimateSize(element); - } - return arraySize; - } - }; - - TypeHandler MAP = new TypeHandler() { - @Override - public Value deserialize(ByteBuffer buffer) throws ImprintException { - VarInt.DecodeResult lengthResult = VarInt.decode(buffer); - int length = lengthResult.getValue(); - - if (length == 0) { - return Value.fromMap(Collections.emptyMap()); - } - - if (buffer.remaining() < 2) { - throw new ImprintException(ErrorType.BUFFER_UNDERFLOW, "Not enough bytes for MAP key/value type codes."); - } - var keyType = TypeCode.fromByte(buffer.get()); - var valueType = TypeCode.fromByte(buffer.get()); - var map = new HashMap(length); - - var keyHandler = keyType.getHandler(); - var valueHandler = valueType.getHandler(); - - //Let handlers consume directly from buffer - for (int i = 0; i < length; i++) { - var keyValue = keyHandler.deserialize(buffer);// Advances buffer - var key = MapKey.fromValue(keyValue); - - var mapInternalValue = valueHandler.deserialize(buffer);//Advances buffer - - map.put(key, mapInternalValue); - } - - return Value.fromMap(map); - } - - @Override - public void serialize(Value value, ByteBuffer buffer) throws ImprintException { - var mapValue = (Value.MapValue) value; - var map = mapValue.getValue(); - VarInt.encode(map.size(), buffer); - - if (map.isEmpty()) { - return; - } - - var iterator = map.entrySet().iterator(); - var first = iterator.next(); - var keyType = first.getKey().getTypeCode(); - var valueType = first.getValue().getTypeCode(); - - buffer.put(keyType.getCode()); - buffer.put(valueType.getCode()); - - serializeMapKey(first.getKey(), buffer); - first.getValue().getTypeCode().getHandler().serialize(first.getValue(), buffer); - - while (iterator.hasNext()) { - var entry = iterator.next(); - if (entry.getKey().getTypeCode() != keyType) { - throw new ImprintException(ErrorType.SCHEMA_ERROR, - "Map keys must have same type code: " + - entry.getKey().getTypeCode() + " != " + keyType); - } - if (entry.getValue().getTypeCode() != valueType) { - throw new ImprintException(ErrorType.SCHEMA_ERROR, - "Map values must have same type code: " + - entry.getValue().getTypeCode() + " != " + valueType); - } - - serializeMapKey(entry.getKey(), buffer); - entry.getValue().getTypeCode().getHandler().serialize(entry.getValue(), buffer); - } - } - - @Override - public int estimateSize(Value value) throws ImprintException { - var mapValue = (Value.MapValue) value; - var map = mapValue.getValue(); - int sizeOfLength = VarInt.encodedLength(map.size()); - if (map.isEmpty()) { - return sizeOfLength; - } - int sizeOfTypeCodes = 2; - int mapSize = sizeOfLength + sizeOfTypeCodes; - - for (var entry : map.entrySet()) { - mapSize += estimateMapKeySize(entry.getKey()); - mapSize += entry.getValue().getTypeCode().getHandler().estimateSize(entry.getValue()); - } - return mapSize; - } - - private void serializeMapKey(MapKey key, ByteBuffer buffer) throws ImprintException { - switch (key.getTypeCode()) { - case INT32: - MapKey.Int32Key int32Key = (MapKey.Int32Key) key; - buffer.putInt(int32Key.getValue()); - break; - - case INT64: - MapKey.Int64Key int64Key = (MapKey.Int64Key) key; - buffer.putLong(int64Key.getValue()); - break; - - case BYTES: - MapKey.BytesKey bytesKey = (MapKey.BytesKey) key; - byte[] bytes = bytesKey.getValue(); - VarInt.encode(bytes.length, buffer); - buffer.put(bytes); - break; - - case STRING: - MapKey.StringKey stringKey = (MapKey.StringKey) key; - byte[] stringBytes = stringKey.getValue().getBytes(java.nio.charset.StandardCharsets.UTF_8); - VarInt.encode(stringBytes.length, buffer); - buffer.put(stringBytes); - break; - - default: - throw new ImprintException(ErrorType.SERIALIZATION_ERROR, - "Invalid map key type: " + key.getTypeCode()); - } - } - - private int estimateMapKeySize(MapKey key) throws ImprintException { - switch (key.getTypeCode()) { - case INT32: return 4; - case INT64: return 8; - case BYTES: - byte[] bytes = ((MapKey.BytesKey) key).getValue(); - return VarInt.encodedLength(bytes.length) + bytes.length; - - case STRING: - var str = ((MapKey.StringKey) key).getValue(); - int utf8Length = str.getBytes(java.nio.charset.StandardCharsets.UTF_8).length; - return VarInt.encodedLength(utf8Length) + utf8Length; - - default: - throw new ImprintException(ErrorType.SERIALIZATION_ERROR, - "Invalid map key type: " + key.getTypeCode()); - } - } - }; -} \ No newline at end of file diff --git a/src/main/java/com/imprint/types/Value.java b/src/main/java/com/imprint/types/Value.java deleted file mode 100644 index 070c497..0000000 --- a/src/main/java/com/imprint/types/Value.java +++ /dev/null @@ -1,468 +0,0 @@ -package com.imprint.types; - -import com.imprint.core.ImprintRecord; -import lombok.EqualsAndHashCode; -import lombok.Getter; - -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -/** - * A value that can be stored in an Imprint record. - */ -public abstract class Value { - - public abstract TypeCode getTypeCode(); - public abstract boolean equals(Object obj); - public abstract int hashCode(); - public abstract String toString(); - - // Factory methods - public static Value nullValue() { - return NullValue.INSTANCE; - } - - public static Value fromBoolean(boolean value) { - return new BoolValue(value); - } - - public static Value fromInt32(int value) { - return new Int32Value(value); - } - - public static Value fromInt64(long value) { - return new Int64Value(value); - } - - public static Value fromFloat32(float value) { - return new Float32Value(value); - } - - public static Value fromFloat64(double value) { - return new Float64Value(value); - } - - public static Value fromBytes(byte[] value) { - return new BytesValue(value); - } - - public static Value fromBytesBuffer(ByteBuffer value) { - return new BytesBufferValue(value); - } - - public static Value fromString(String value) { - return new StringValue(value); - } - - public static Value fromStringBuffer(ByteBuffer value) { - return new StringBufferValue(value); - } - - - public static Value fromArray(List value) { - return new ArrayValue(value); - } - - public static Value fromMap(Map value) { - return new MapValue(value); - } - - public static Value fromRow(ImprintRecord value) { - return new RowValue(value); - } - - // Null Value - @EqualsAndHashCode(callSuper = false) - public static class NullValue extends Value { - public static final NullValue INSTANCE = new NullValue(); - - private NullValue() {} - - @Override - public TypeCode getTypeCode() { return TypeCode.NULL; } - - @Override - public String toString() { - return "null"; - } - } - - // Boolean Value - @Getter - @EqualsAndHashCode(callSuper = false) - public static class BoolValue extends Value { - private final boolean value; - - public BoolValue(boolean value) { - this.value = value; - } - - public boolean getValue() { return value; } - - @Override - public TypeCode getTypeCode() { return TypeCode.BOOL; } - - @Override - public String toString() { - return String.valueOf(value); - } - } - - // Int32 Value - @Getter - @EqualsAndHashCode(callSuper = false) - public static class Int32Value extends Value { - private final int value; - - public Int32Value(int value) { - this.value = value; - } - - @Override - public TypeCode getTypeCode() { return TypeCode.INT32; } - - @Override - public String toString() { - return String.valueOf(value); - } - } - - // Int64 Value - @Getter - @EqualsAndHashCode(callSuper = false) - public static class Int64Value extends Value { - private final long value; - - public Int64Value(long value) { - this.value = value; - } - - @Override - public TypeCode getTypeCode() { return TypeCode.INT64; } - - @Override - public String toString() { - return String.valueOf(value); - } - } - - // Float32 Value - @Getter - @EqualsAndHashCode(callSuper = false) - public static class Float32Value extends Value { - private final float value; - - public Float32Value(float value) { - this.value = value; - } - - @Override - public TypeCode getTypeCode() { return TypeCode.FLOAT32; } - - @Override - public String toString() { - return String.valueOf(value); - } - } - - // Float64 Value - - @Getter - @EqualsAndHashCode(callSuper = false) - public static class Float64Value extends Value { - private final double value; - - public Float64Value(double value) { - this.value = value; - } - - @Override - public TypeCode getTypeCode() { return TypeCode.FLOAT64; } - - @Override - public String toString() { - return String.valueOf(value); - } - } - - // Bytes Value (array-based) - @Getter - public static class BytesValue extends Value { - /** - * Returns internal array. MUST NOT be modified by caller. - */ - private final byte[] value; - - /** - * Takes ownership of the byte array. Caller must not modify after construction. - */ - public BytesValue(byte[] value) { - this.value = Objects.requireNonNull(value); - } - - @Override - public TypeCode getTypeCode() { return TypeCode.BYTES; } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (obj instanceof BytesValue) { - BytesValue that = (BytesValue) obj; - return Arrays.equals(value, that.value); - } - if (obj instanceof BytesBufferValue) { - BytesBufferValue that = (BytesBufferValue) obj; - return Arrays.equals(value, that.getValue()); - } - return false; - } - - @Override - public int hashCode() { - return Arrays.hashCode(value); - } - - @Override - public String toString() { - return "bytes[" + value.length + "]"; - } - } - - // Bytes Value (ByteBuffer-based, zero-copy) - public static class BytesBufferValue extends Value { - private final ByteBuffer value; - - public BytesBufferValue(ByteBuffer value) { - this.value = value.asReadOnlyBuffer(); - } - - public byte[] getValue() { - // Fallback to array when needed - byte[] array = new byte[value.remaining()]; - value.duplicate().get(array); - return array; - } - - public ByteBuffer getBuffer() { - return value.duplicate(); - } - - @Override - public TypeCode getTypeCode() { return TypeCode.BYTES; } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (obj instanceof BytesBufferValue) { - BytesBufferValue that = (BytesBufferValue) obj; - return value.equals(that.value); - } - if (obj instanceof BytesValue) { - BytesValue that = (BytesValue) obj; - return Arrays.equals(getValue(), that.getValue()); - } - return false; - } - - @Override - public int hashCode() { - return value.hashCode(); - } - - @Override - public String toString() { - return "bytes[" + value.remaining() + "]"; - } - } - - // String Value (String-based) - public static class StringValue extends Value { - @Getter - private final String value; - private byte[] utf8BytesCache; - - public StringValue(String value) { - this.value = Objects.requireNonNull(value, "String cannot be null"); - } - - public byte[] getUtf8Bytes() { - if (utf8BytesCache == null) { - utf8BytesCache = value.getBytes(StandardCharsets.UTF_8); - } - return utf8BytesCache; - } - - public int getUtf8Length() { - return getUtf8Bytes().length; - } - - @Override - public TypeCode getTypeCode() { return TypeCode.STRING; } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (obj instanceof StringValue) { - StringValue that = (StringValue) obj; - return value.equals(that.value); - } - if (obj instanceof StringBufferValue) { - StringBufferValue that = (StringBufferValue) obj; - return value.equals(that.getValue()); - } - return false; - } - - @Override - public int hashCode() { - return value.hashCode(); - } - - @Override - public String toString() { - return "\"" + value + "\""; - } - } - - // String Value (ByteBuffer-based) - public static class StringBufferValue extends Value { - private final ByteBuffer value; - private String cachedString; - - private static final int THREAD_LOCAL_BUFFER_SIZE = 1024; - private static final ThreadLocal DECODE_BUFFER_CACHE = - ThreadLocal.withInitial(() -> new byte[THREAD_LOCAL_BUFFER_SIZE]); - - public StringBufferValue(ByteBuffer value) { - this.value = value.asReadOnlyBuffer(); - } - - public String getValue() { - String result = cachedString; - if (result == null) { - result = decodeUtf8(); - cachedString = result; - } - return result; - } - - private String decodeUtf8() { - final byte[] array; - final int offset; - final int length = value.remaining(); - - if (value.hasArray()) { - array = value.array(); - offset = value.arrayOffset() + value.position(); - } else { - byte[] threadLocalBuffer = DECODE_BUFFER_CACHE.get(); - if (length <= threadLocalBuffer.length) { - array = threadLocalBuffer; - } else { - // Fallback: copy bytes from the ByteBuffer to a new heap array (if too large for cache) - array = new byte[length]; - } - value.duplicate().get(array, 0, length); - offset = 0; - } - return new String(array, offset, length, StandardCharsets.UTF_8); - } - - public ByteBuffer getBuffer() { - return value.duplicate(); - } - - @Override - public TypeCode getTypeCode() { return TypeCode.STRING; } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (obj instanceof StringBufferValue) { - StringBufferValue that = (StringBufferValue) obj; - return value.equals(that.value); - } - if (obj instanceof StringValue) { - StringValue that = (StringValue) obj; - return getValue().equals(that.getValue()); - } - return false; - } - - @Override - public int hashCode() { - return getValue().hashCode(); // Use string hash for consistency - } - - @Override - public String toString() { - return "\"" + getValue() + "\""; - } - } - - // Array Value - @Getter - @EqualsAndHashCode(callSuper = false) - public static class ArrayValue extends Value { - private final List value; - - public ArrayValue(List value) { - this.value = List.copyOf(Objects.requireNonNull(value, "Array cannot be null")); - } - - @Override - public TypeCode getTypeCode() { return TypeCode.ARRAY; } - - @Override - public String toString() { - return value.toString(); - } - } - - // Map Value - @Getter - @EqualsAndHashCode(callSuper = false) - public static class MapValue extends Value { - private final Map value; - - public MapValue(Map value) { - this.value = Map.copyOf(Objects.requireNonNull(value, "Map cannot be null")); - } - - @Override - public TypeCode getTypeCode() { return TypeCode.MAP; } - - @Override - public String toString() { - return value.toString(); - } - } - - // Row Value - @Getter - @EqualsAndHashCode(callSuper = false) - public static class RowValue extends Value { - private final ImprintRecord value; - - public RowValue(ImprintRecord value) { - this.value = Objects.requireNonNull(value, "Record cannot be null"); - } - - @Override - public TypeCode getTypeCode() { return TypeCode.ROW; } - - @Override - public String toString() { - return "Row{" + value + "}"; - } - } - -} \ No newline at end of file diff --git a/src/main/java/com/imprint/util/VarInt.java b/src/main/java/com/imprint/util/VarInt.java index 70c9095..29dd4d3 100644 --- a/src/main/java/com/imprint/util/VarInt.java +++ b/src/main/java/com/imprint/util/VarInt.java @@ -2,10 +2,7 @@ import com.imprint.error.ImprintException; import com.imprint.error.ErrorType; -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.ToString; +import lombok.*; import lombok.experimental.UtilityClass; import java.nio.ByteBuffer; @@ -46,14 +43,12 @@ public final class VarInt { public static void encode(int value, ByteBuffer buffer) { // Convert to unsigned long for proper bit manipulation long val = Integer.toUnsignedLong(value); - // Encode at least one byte, then continue while value has more bits do { byte b = (byte) (val & SEGMENT_BITS); val >>>= 7; - if (val != 0) { + if (val != 0) b |= CONTINUATION_BIT; - } buffer.put(b); } while (val != 0); } @@ -80,18 +75,14 @@ public static DecodeResult decode(ByteBuffer buffer) throws ImprintException { // Check if adding these 7 bits would overflow long segment = b & SEGMENT_BITS; - if (shift >= 32 || (shift == 28 && segment > 0xF)) { + if (shift >= 32 || (shift == 28 && segment > 0xF)) throw new ImprintException(ErrorType.MALFORMED_VARINT, "VarInt overflow"); - } - // Add the bottom 7 bits to the result result |= segment << shift; // If the high bit is not set, this is the last byte - if ((b & CONTINUATION_BIT) == 0) { + if ((b & CONTINUATION_BIT) == 0) break; - } - shift += 7; } @@ -104,10 +95,8 @@ public static DecodeResult decode(ByteBuffer buffer) throws ImprintException { * @return the number of bytes needed */ public static int encodedLength(int value) { - if (value >= 0 && value < CACHE_SIZE) { + if (value >= 0 && value < CACHE_SIZE) return ENCODED_LENGTHS[value]; - } - long val = Integer.toUnsignedLong(value); int length = 1; while (val >= 0x80) { @@ -120,12 +109,9 @@ public static int encodedLength(int value) { /** * Result of a VarInt decode operation. */ - @Getter - @AllArgsConstructor - @EqualsAndHashCode - @ToString + @Value public static class DecodeResult { - private final int value; - private final int bytesRead; + int value; + int bytesRead; } } \ No newline at end of file diff --git a/src/test/java/com/imprint/IntegrationTest.java b/src/test/java/com/imprint/IntegrationTest.java index e066f01..cc70873 100644 --- a/src/test/java/com/imprint/IntegrationTest.java +++ b/src/test/java/com/imprint/IntegrationTest.java @@ -75,19 +75,19 @@ var record = ImprintRecord.builder(schemaId) ImprintRecord deserialized = ImprintRecord.deserialize(serialized); // Verify array - List deserializedArray = deserialized.getArray(1); + List deserializedArray = deserialized.getArray(1); assertNotNull(deserializedArray); assertEquals(3, deserializedArray.size()); - assertEquals(Value.fromInt32(1), deserializedArray.get(0)); - assertEquals(Value.fromInt32(2), deserializedArray.get(1)); - assertEquals(Value.fromInt32(3), deserializedArray.get(2)); + assertEquals(Integer.valueOf(1), deserializedArray.get(0)); + assertEquals(Integer.valueOf(2), deserializedArray.get(1)); + assertEquals(Integer.valueOf(3), deserializedArray.get(2)); // Verify map - Map deserializedMap = deserialized.getMap(2); + Map deserializedMap = deserialized.getMap(2); assertNotNull(deserializedMap); assertEquals(2, deserializedMap.size()); - assertEquals(Value.fromInt32(1), deserializedMap.get(MapKey.fromString("one"))); - assertEquals(Value.fromInt32(2), deserializedMap.get(MapKey.fromString("two"))); + assertEquals(Integer.valueOf(1), deserializedMap.get("one")); + assertEquals(Integer.valueOf(2), deserializedMap.get("two")); } @Test @@ -168,18 +168,18 @@ void testProjectComplexTypes() throws ImprintException { .field(100, "nested value") .build(); - // Create homogeneous array (all strings) - var testArray = Arrays.asList(Value.fromString("item1"), Value.fromString("item2"), Value.fromString("item3")); + // Create homogeneous array (all strings) - builder will handle conversion + var testArray = Arrays.asList("item1", "item2", "item3"); - // Create homogeneous map (string keys -> string values) - var testMap = new HashMap(); - testMap.put(MapKey.fromString("key1"), Value.fromString("value1")); - testMap.put(MapKey.fromString("key2"), Value.fromString("value2")); + // Create homogeneous map (string keys -> string values) - builder will handle conversion + var testMap = new HashMap(); + testMap.put("key1", "value1"); + testMap.put("key2", "value2"); var originalRecord = ImprintRecord.builder(schemaId) .field(1, "simple string") - .field(2, Value.fromArray(testArray)) - .field(3, Value.fromMap(testMap)) + .field(2, testArray) + .field(3, testMap) .field(4, nestedRecord) .field(5, 999L) .build(); @@ -192,15 +192,15 @@ void testProjectComplexTypes() throws ImprintException { // Verify array projection (homogeneous strings) var projectedArray = projected.getArray(2); assertEquals(3, projectedArray.size()); - assertEquals(Value.fromString("item1"), projectedArray.get(0)); - assertEquals(Value.fromString("item2"), projectedArray.get(1)); - assertEquals(Value.fromString("item3"), projectedArray.get(2)); + assertEquals("item1", projectedArray.get(0)); + assertEquals("item2", projectedArray.get(1)); + assertEquals("item3", projectedArray.get(2)); // Verify map projection (string -> string) var projectedMap = projected.getMap(3); assertEquals(2, projectedMap.size()); - assertEquals(Value.fromString("value1"), projectedMap.get(MapKey.fromString("key1"))); - assertEquals(Value.fromString("value2"), projectedMap.get(MapKey.fromString("key2"))); + assertEquals("value1", projectedMap.get("key1")); + assertEquals("value2", projectedMap.get("key2")); // Verify nested record projection var projectedNested = projected.getRow(4); @@ -294,27 +294,27 @@ void testMergeComplexTypes() throws ImprintException { .field(200, "nested in record2") .build(); - // Create arrays - var array1 = Arrays.asList(Value.fromString("array1_item1"), Value.fromString("array1_item2")); - var array2 = Arrays.asList(Value.fromInt32(10), Value.fromInt32(20)); + // Create arrays - builder will handle conversion + var array1 = Arrays.asList("array1_item1", "array1_item2"); + var array2 = Arrays.asList(10, 20); - // Create maps - var map1 = new HashMap(); - map1.put(MapKey.fromString("map1_key"), Value.fromString("map1_value")); + // Create maps - builder will handle conversion + var map1 = new HashMap(); + map1.put("map1_key", "map1_value"); - var map2 = new HashMap(); - map2.put(MapKey.fromInt32(42), Value.fromBoolean(true)); + var map2 = new HashMap(); + map2.put(42, true); var record1 = ImprintRecord.builder(schemaId) .field(1, nested1) - .field(3, Value.fromArray(array1)) - .field(5, Value.fromMap(map1)) + .field(3, array1) + .field(5, map1) .build(); var record2 = ImprintRecord.builder(schemaId) .field(2, nested2) - .field(4, Value.fromArray(array2)) - .field(6, Value.fromMap(map2)) + .field(4, array2) + .field(6, map2) .build(); var merged = record1.merge(record2); @@ -331,18 +331,18 @@ void testMergeComplexTypes() throws ImprintException { // Verify arrays var mergedArray1 = merged.getArray(3); assertEquals(2, mergedArray1.size()); - assertEquals(Value.fromString("array1_item1"), mergedArray1.get(0)); + assertEquals("array1_item1", mergedArray1.get(0)); var mergedArray2 = merged.getArray(4); assertEquals(2, mergedArray2.size()); - assertEquals(Value.fromInt32(10), mergedArray2.get(0)); + assertEquals(10, mergedArray2.get(0)); // Verify maps var mergedMap1 = merged.getMap(5); - assertEquals(Value.fromString("map1_value"), mergedMap1.get(MapKey.fromString("map1_key"))); + assertEquals("map1_value", mergedMap1.get("map1_key")); var mergedMap2 = merged.getMap(6); - assertEquals(Value.fromBoolean(true), mergedMap2.get(MapKey.fromInt32(42))); + assertEquals(true, mergedMap2.get(42)); } @Test @@ -465,12 +465,14 @@ void testLargeRecordOperations() throws ImprintException { private ImprintRecord createTestRecordForGetters() throws ImprintException { SchemaId schemaId = new SchemaId(5, 0xabcdef01); - List innerList1 = Arrays.asList(Value.fromInt32(10), Value.fromInt32(20)); - List innerList2 = Arrays.asList(Value.fromInt32(30), Value.fromInt32(40)); - List listOfLists = Arrays.asList(Value.fromArray(innerList1), Value.fromArray(innerList2)); + // Create nested arrays - builder will handle conversion + List innerList1 = Arrays.asList(10, 20); + List innerList2 = Arrays.asList(30, 40); + List> listOfLists = Arrays.asList(innerList1, innerList2); - Map mapWithArrayValue = new HashMap<>(); - mapWithArrayValue.put(MapKey.fromString("list1"), Value.fromArray(innerList1)); + // Create map with array value - builder will handle conversion + Map> mapWithArrayValue = new HashMap<>(); + mapWithArrayValue.put("list1", innerList1); return ImprintRecord.builder(schemaId) .field(1, true) @@ -481,8 +483,8 @@ private ImprintRecord createTestRecordForGetters() throws ImprintException { .field(6, "hello type world") .field(7, new byte[]{10, 20, 30}) .nullField(8) - .field(9, Value.fromArray(listOfLists)) // Array of Arrays (using Value directly for test setup) - .field(10, Value.fromMap(mapWithArrayValue)) // Map with Array value + .field(9, listOfLists) // Array of Arrays - builder handles conversion + .field(10, mapWithArrayValue) // Map with Array value - builder handles conversion .field(11, Collections.emptyList()) // Empty Array via builder .field(12, Collections.emptyMap()) // Empty Map via builder .build(); @@ -516,20 +518,21 @@ void testTypeGetterArrayOfArrays() throws ImprintException { var originalRecord = createTestRecordForGetters(); var record = serializeAndDeserialize(originalRecord); - List arrOfArr = record.getArray(9); + List> arrOfArr = record.getArray(9); assertNotNull(arrOfArr); assertEquals(2, arrOfArr.size()); - assertInstanceOf(Value.ArrayValue.class, arrOfArr.get(0)); - Value.ArrayValue firstInnerArray = (Value.ArrayValue) arrOfArr.get(0); - assertEquals(2, firstInnerArray.getValue().size()); - assertEquals(Value.fromInt32(10), firstInnerArray.getValue().get(0)); - assertEquals(Value.fromInt32(20), firstInnerArray.getValue().get(1)); - - assertInstanceOf(Value.ArrayValue.class, arrOfArr.get(1)); - Value.ArrayValue secondInnerArray = (Value.ArrayValue) arrOfArr.get(1); - assertEquals(2, secondInnerArray.getValue().size()); - assertEquals(Value.fromInt32(30), secondInnerArray.getValue().get(0)); - assertEquals(Value.fromInt32(40), secondInnerArray.getValue().get(1)); + + List firstInnerArray = arrOfArr.get(0); + assertNotNull(firstInnerArray); + assertEquals(2, firstInnerArray.size()); + assertEquals(Integer.valueOf(10), firstInnerArray.get(0)); + assertEquals(Integer.valueOf(20), firstInnerArray.get(1)); + + List secondInnerArray = arrOfArr.get(1); + assertNotNull(secondInnerArray); + assertEquals(2, secondInnerArray.size()); + assertEquals(Integer.valueOf(30), secondInnerArray.get(0)); + assertEquals(Integer.valueOf(40), secondInnerArray.get(1)); } @Test @@ -538,14 +541,14 @@ void testTypeGetterMapWithArrayValue() throws ImprintException { var originalRecord = createTestRecordForGetters(); var record = serializeAndDeserialize(originalRecord); - Map mapWithArr = record.getMap(10); + Map> mapWithArr = record.getMap(10); assertNotNull(mapWithArr); assertEquals(1, mapWithArr.size()); - assertInstanceOf(Value.ArrayValue.class, mapWithArr.get(MapKey.fromString("list1"))); - Value.ArrayValue innerArray = (Value.ArrayValue) mapWithArr.get(MapKey.fromString("list1")); + + List innerArray = mapWithArr.get("list1"); assertNotNull(innerArray); - assertEquals(2, innerArray.getValue().size()); - assertEquals(Value.fromInt32(10), innerArray.getValue().get(0)); + assertEquals(2, innerArray.size()); + assertEquals(Integer.valueOf(10), innerArray.get(0)); } @Test @@ -554,11 +557,11 @@ void testTypeGettersEmptyCollections() throws ImprintException { var originalRecord = createTestRecordForGetters(); var record = serializeAndDeserialize(originalRecord); - List emptyArr = record.getArray(11); + List emptyArr = record.getArray(11); assertNotNull(emptyArr); assertTrue(emptyArr.isEmpty()); - Map emptyMap = record.getMap(12); + Map emptyMap = record.getMap(12); assertNotNull(emptyMap); assertTrue(emptyMap.isEmpty()); } @@ -584,10 +587,9 @@ var record = serializeAndDeserialize(originalRecord); assertTrue(ex.getMessage().contains("Field 8 is NULL")); - // Also test getValue for a null field returns Value.NullValue - Value nullValueField = record.getValue(8); - assertNotNull(nullValueField); - assertInstanceOf(Value.NullValue.class, nullValueField, "Field 8 should be Value.NullValue"); + // Also test getValue for a null field returns null + Object nullValueField = record.getValue(8); + assertNull(nullValueField, "Field 8 should be null"); } @Test @@ -743,37 +745,49 @@ void testDeepNesting() throws ImprintException { void testMapKeyTypeVariations() throws ImprintException { var schemaId = new SchemaId(70, 0xAAB5E75); - // Create maps with different key types - var stringKeyMap = new HashMap(); - stringKeyMap.put(MapKey.fromString("string_key"), Value.fromString("string_value")); + // Create maps with different key types - use simple types for builder + var stringKeyMap = new HashMap(); + stringKeyMap.put("string_key", "string_value"); - var intKeyMap = new HashMap(); - intKeyMap.put(MapKey.fromInt32(42), Value.fromString("int_value")); + var intKeyMap = new HashMap(); + intKeyMap.put(42, "int_value"); - var longKeyMap = new HashMap(); - longKeyMap.put(MapKey.fromInt64(9876543210L), Value.fromString("long_value")); + var longKeyMap = new HashMap(); + longKeyMap.put(9876543210L, "long_value"); - var bytesKeyMap = new HashMap(); - bytesKeyMap.put(MapKey.fromBytes(new byte[]{1, 2, 3}), Value.fromString("bytes_value")); + var bytesKeyMap = new HashMap(); + bytesKeyMap.put(new byte[]{1, 2, 3}, "bytes_value"); var record = ImprintRecord.builder(schemaId) - .field(1, Value.fromMap(stringKeyMap)) - .field(2, Value.fromMap(intKeyMap)) - .field(3, Value.fromMap(longKeyMap)) - .field(4, Value.fromMap(bytesKeyMap)) + .field(1, stringKeyMap) + .field(2, intKeyMap) + .field(3, longKeyMap) + .field(4, bytesKeyMap) .build(); var deserialized = serializeAndDeserialize(record); // Verify all map key types work correctly - assertEquals(Value.fromString("string_value"), - deserialized.getMap(1).get(MapKey.fromString("string_key"))); - assertEquals(Value.fromString("int_value"), - deserialized.getMap(2).get(MapKey.fromInt32(42))); - assertEquals(Value.fromString("long_value"), - deserialized.getMap(3).get(MapKey.fromInt64(9876543210L))); - assertEquals(Value.fromString("bytes_value"), - deserialized.getMap(4).get(MapKey.fromBytes(new byte[]{1, 2, 3}))); + assertEquals("string_value", + deserialized.getMap(1).get("string_key")); + assertEquals("int_value", + deserialized.getMap(2).get(42)); + assertEquals("long_value", + deserialized.getMap(3).get(9876543210L)); + // For byte array keys, we need to find the entry since arrays use reference equality + Map bytesKeyedMap = deserialized.getMap(4); + assertEquals(1, bytesKeyedMap.size()); + // The key should be a byte array {1, 2, 3} and the value should be "bytes_value" + byte[] expectedBytes = {1, 2, 3}; + Object actualValue = null; + for (Map.Entry entry : bytesKeyedMap.entrySet()) { + byte[] keyBytes = (byte[]) entry.getKey(); + if (java.util.Arrays.equals(keyBytes, expectedBytes)) { + actualValue = entry.getValue(); + break; + } + } + assertEquals("bytes_value", actualValue); } @Test diff --git a/src/test/java/com/imprint/ops/ImprintOperationsTest.java b/src/test/java/com/imprint/ops/ImprintOperationsTest.java index 292f8f3..4821125 100644 --- a/src/test/java/com/imprint/ops/ImprintOperationsTest.java +++ b/src/test/java/com/imprint/ops/ImprintOperationsTest.java @@ -4,7 +4,6 @@ import com.imprint.core.ImprintRecord; import com.imprint.core.SchemaId; import com.imprint.error.ImprintException; -import com.imprint.types.Value; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -109,10 +108,17 @@ void shouldPreserveAllFieldsWhenProjectingAll() throws ImprintException { assertEquals(multiFieldRecord.getDirectory().size(), projected.getDirectory().size()); for (Directory entry : multiFieldRecord.getDirectory()) { - Value originalValue = multiFieldRecord.getValue(entry.getId()); - Value projectedValue = projected.getValue(entry.getId()); - assertEquals(originalValue, projectedValue, - "Field " + entry.getId() + " should have matching value"); + Object originalValue = multiFieldRecord.getValue(entry.getId()); + Object projectedValue = projected.getValue(entry.getId()); + + // Handle byte arrays specially since they don't use content equality + if (originalValue instanceof byte[] && projectedValue instanceof byte[]) { + assertArrayEquals((byte[]) originalValue, (byte[]) projectedValue, + "Field " + entry.getId() + " byte array should have matching content"); + } else { + assertEquals(originalValue, projectedValue, + "Field " + entry.getId() + " should have matching value"); + } } } @@ -298,9 +304,22 @@ void shouldHandleMergeWithEmptyRecord() throws ImprintException { // And values should be preserved for (Directory entry : multiFieldRecord.getDirectory()) { - Value originalValue = multiFieldRecord.getValue(entry.getId()); - assertEquals(originalValue, merged1.getValue(entry.getId())); - assertEquals(originalValue, merged2.getValue(entry.getId())); + Object originalValue = multiFieldRecord.getValue(entry.getId()); + Object merged1Value = merged1.getValue(entry.getId()); + Object merged2Value = merged2.getValue(entry.getId()); + + // Handle byte arrays specially since they don't use content equality + if (originalValue instanceof byte[]) { + assertArrayEquals((byte[]) originalValue, (byte[]) merged1Value, + "Field " + entry.getId() + " should be preserved in merged1"); + assertArrayEquals((byte[]) originalValue, (byte[]) merged2Value, + "Field " + entry.getId() + " should be preserved in merged2"); + } else { + assertEquals(originalValue, merged1Value, + "Field " + entry.getId() + " should be preserved in merged1"); + assertEquals(originalValue, merged2Value, + "Field " + entry.getId() + " should be preserved in merged2"); + } } } diff --git a/src/test/java/com/imprint/profile/ProfilerTest.java b/src/test/java/com/imprint/profile/ProfilerTest.java index 79882d9..5c38457 100644 --- a/src/test/java/com/imprint/profile/ProfilerTest.java +++ b/src/test/java/com/imprint/profile/ProfilerTest.java @@ -2,14 +2,10 @@ import com.imprint.core.ImprintRecord; import com.imprint.core.SchemaId; -import com.imprint.ops.ImprintOperations; -import com.imprint.types.Value; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertTrue; - import java.util.Random; import java.util.stream.IntStream; @@ -136,7 +132,7 @@ private void profileDisjointMerges() throws Exception { @Tag("serialization") @Tag("small-records") void profileSmallRecordSerialization() throws Exception { - profileSerialization("small records", RECORD_SIZE, 100_000); + profileSerialization("small records", RECORD_SIZE, 600_000); } @Test @@ -271,25 +267,25 @@ private void profileSerialization(String testName, int recordSize, int iteration for (int fieldId = 1; fieldId <= recordSize; fieldId++) { switch (fieldId % 7) { case 0: - builder.field(fieldId, Value.fromInt32(i + fieldId)); + builder.field(fieldId, i + fieldId); break; case 1: - builder.field(fieldId, Value.fromInt64(i * 1000L + fieldId)); + builder.field(fieldId, i * 1000L + fieldId); break; case 2: - builder.field(fieldId, Value.fromString("test-string-" + i + "-" + fieldId)); + builder.field(fieldId, "test-string-" + i + "-" + fieldId); break; case 3: - builder.field(fieldId, Value.fromString("longer-descriptive-text-for-field-" + fieldId + "-iteration-" + i)); + builder.field(fieldId, "longer-descriptive-text-for-field-" + fieldId + "-iteration-" + i); break; case 4: - builder.field(fieldId, Value.fromFloat64(i * 3.14159 + fieldId)); + builder.field(fieldId, i * 3.14159 + fieldId); break; case 5: - builder.field(fieldId, Value.fromBytes(("bytes-" + i + "-" + fieldId).getBytes())); + builder.field(fieldId, ("bytes-" + i + "-" + fieldId).getBytes()); break; case 6: - builder.field(fieldId, Value.fromBoolean((i + fieldId) % 2 == 0)); + builder.field(fieldId, (i + fieldId) % 2 == 0); break; } } @@ -324,16 +320,16 @@ private ImprintRecord createTestRecord(int recordSize) throws Exception { for (int i = 1; i <= recordSize; i++) { switch (i % 4) { case 0: - builder.field(i, Value.fromInt32(i * 100)); + builder.field(i, i * 100); break; case 1: - builder.field(i, Value.fromString("field-value-" + i)); + builder.field(i, "field-value-" + i); break; case 2: - builder.field(i, Value.fromFloat64(i * 3.14159)); + builder.field(i, i * 3.14159); break; case 3: - builder.field(i, Value.fromBytes(("bytes-" + i).getBytes())); + builder.field(i, ("bytes-" + i).getBytes()); break; } } @@ -346,16 +342,16 @@ private ImprintRecord createTestRecordWithFieldIds(int[] fieldIds) throws Except for (int fieldId : fieldIds) { switch (fieldId % 4) { case 0: - builder.field(fieldId, Value.fromInt32(fieldId * 100)); + builder.field(fieldId, fieldId * 100); break; case 1: - builder.field(fieldId, Value.fromString("field-value-" + fieldId)); + builder.field(fieldId, "field-value-" + fieldId); break; case 2: - builder.field(fieldId, Value.fromFloat64(fieldId * 3.14159)); + builder.field(fieldId, fieldId * 3.14159); break; case 3: - builder.field(fieldId, Value.fromBytes(("bytes-" + fieldId).getBytes())); + builder.field(fieldId, ("bytes-" + fieldId).getBytes()); break; } } @@ -379,59 +375,4 @@ private int[] generateRandomFields(Random random, int maxField, int count) { .sorted() .toArray(); } - - @Test - @Tag("profiling") - void profileBytesToBytesVsObjectMerge() throws Exception { - System.out.println("=== Bytes-to-Bytes vs Object Merge Comparison ==="); - - // Create test records - var record1 = createTestRecordWithFieldIds(new int[]{1, 3, 5, 7, 9, 11, 13, 15}); - var record2 = createTestRecordWithFieldIds(new int[]{2, 4, 6, 8, 10, 12, 14, 16}); - - var record1Bytes = record1.serializeToBuffer(); - var record2Bytes = record2.serializeToBuffer(); - - int iterations = 50_000; - - // Warm up - for (int i = 0; i < 1000; i++) { - record1.merge(record2).serializeToBuffer(); - ImprintOperations.mergeBytes(record1Bytes, record2Bytes); - } - - System.out.printf("Profiling %,d merge operations...%n", iterations); - - // Test object merge + serialize - long startObjectMerge = System.nanoTime(); - for (int i = 0; i < iterations; i++) { - var merged = record1.merge(record2); - var serialized = merged.serializeToBuffer(); - // Consume result to prevent optimization - if (serialized.remaining() == 0) throw new RuntimeException("Empty result"); - } - long objectMergeTime = System.nanoTime() - startObjectMerge; - - // Test bytes merge - long startBytesMerge = System.nanoTime(); - for (int i = 0; i < iterations; i++) { - var merged = ImprintOperations.mergeBytes(record1Bytes, record2Bytes); - // Consume result to prevent optimization - if (merged.remaining() == 0) throw new RuntimeException("Empty result"); - } - long bytesMergeTime = System.nanoTime() - startBytesMerge; - - double objectAvg = (double) objectMergeTime / iterations / 1000.0; // microseconds - double bytesAvg = (double) bytesMergeTime / iterations / 1000.0; // microseconds - double speedup = objectAvg / bytesAvg; - - System.out.printf("Object merge + serialize: %.2f ms (avg: %.1f μs/op)%n", - objectMergeTime / 1_000_000.0, objectAvg); - System.out.printf("Bytes-to-bytes merge: %.2f ms (avg: %.1f μs/op)%n", - bytesMergeTime / 1_000_000.0, bytesAvg); - System.out.printf("Speedup: %.1fx faster%n", speedup); - - // Assert that bytes approach is faster (should be at least 1.5x) - assertTrue(speedup > 1.0, String.format("Bytes merge should be faster. Got %.1fx speedup", speedup)); - } } \ No newline at end of file diff --git a/src/test/java/com/imprint/types/MapKeyTest.java b/src/test/java/com/imprint/types/MapKeyTest.java index 08f4180..f9707e4 100644 --- a/src/test/java/com/imprint/types/MapKeyTest.java +++ b/src/test/java/com/imprint/types/MapKeyTest.java @@ -8,11 +8,11 @@ class MapKeyTest { @Test - void shouldCreateMapKeysFromValues() throws ImprintException { - var int32Key = MapKey.fromValue(Value.fromInt32(42)); - var int64Key = MapKey.fromValue(Value.fromInt64(123L)); - var bytesKey = MapKey.fromValue(Value.fromBytes(new byte[]{1, 2, 3})); - var stringKey = MapKey.fromValue(Value.fromString("test")); + void shouldCreateMapKeysFromPrimitives() throws ImprintException { + var int32Key = MapKey.fromPrimitive(TypeCode.INT32, 42); + var int64Key = MapKey.fromPrimitive(TypeCode.INT64, 123L); + var bytesKey = MapKey.fromPrimitive(TypeCode.BYTES, new byte[]{1, 2, 3}); + var stringKey = MapKey.fromPrimitive(TypeCode.STRING, "test"); assertThat(int32Key).isInstanceOf(MapKey.Int32Key.class); assertThat(((MapKey.Int32Key) int32Key).getValue()).isEqualTo(42); @@ -28,31 +28,28 @@ void shouldCreateMapKeysFromValues() throws ImprintException { } @Test - void shouldConvertBackToValues() { + void shouldConvertToPrimitives() { var int32Key = MapKey.fromInt32(42); var stringKey = MapKey.fromString("test"); - var int32Value = int32Key.toValue(); - var stringValue = stringKey.toValue(); + Object int32Value = int32Key.getPrimitiveValue(); + Object stringValue = stringKey.getPrimitiveValue(); - assertThat(int32Value).isInstanceOf(Value.Int32Value.class); - assertThat(((Value.Int32Value) int32Value).getValue()).isEqualTo(42); + assertThat(int32Value).isInstanceOf(Integer.class); + assertThat(int32Value).isEqualTo(42); - assertThat(stringValue).isInstanceOf(Value.StringValue.class); - assertThat(((Value.StringValue) stringValue).getValue()).isEqualTo("test"); + assertThat(stringValue).isInstanceOf(String.class); + assertThat(stringValue).isEqualTo("test"); } @Test - void shouldRejectInvalidValueTypes() { - var boolValue = Value.fromBoolean(true); - var arrayValue = Value.fromArray(java.util.Collections.emptyList()); - - assertThatThrownBy(() -> MapKey.fromValue(boolValue)) + void shouldRejectInvalidPrimitiveTypes() { + assertThatThrownBy(() -> MapKey.fromPrimitive(TypeCode.BOOL, true)) .isInstanceOf(ImprintException.class) .extracting("errorType") .isEqualTo(ErrorType.TYPE_MISMATCH); - assertThatThrownBy(() -> MapKey.fromValue(arrayValue)) + assertThatThrownBy(() -> MapKey.fromPrimitive(TypeCode.ARRAY, java.util.Collections.emptyList())) .isInstanceOf(ImprintException.class) .extracting("errorType") .isEqualTo(ErrorType.TYPE_MISMATCH); diff --git a/src/test/java/com/imprint/types/TypeHandlerTest.java b/src/test/java/com/imprint/types/TypeHandlerTest.java deleted file mode 100644 index 75d118f..0000000 --- a/src/test/java/com/imprint/types/TypeHandlerTest.java +++ /dev/null @@ -1,274 +0,0 @@ -package com.imprint.types; - -import com.imprint.error.ImprintException; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -import static org.assertj.core.api.Assertions.*; - -/** - * Tests for individual TypeHandler implementations. - * Validates serialization, deserialization, and size estimation for each type. - */ -class TypeHandlerTest { - - @Test - void testNullHandler() throws ImprintException { - var handler = TypeHandler.NULL; - var value = Value.nullValue(); - - // Size estimation - assertThat(handler.estimateSize(value)).isEqualTo(0); - - // Serialization - var buffer = ByteBuffer.allocate(10); - handler.serialize(value, buffer); - assertThat(buffer.position()).isEqualTo(0); // NULL writes nothing - - // Deserialization - buffer.flip(); - var deserialized = handler.deserialize(buffer); - assertThat(deserialized).isEqualTo(value); - } - - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void testBoolHandler(boolean testValue) throws ImprintException { - var handler = TypeHandler.BOOL; - var value = Value.fromBoolean(testValue); - - // Size estimation - assertThat(handler.estimateSize(value)).isEqualTo(1); - - // Round-trip test - var buffer = ByteBuffer.allocate(10); - handler.serialize(value, buffer); - assertThat(buffer.position()).isEqualTo(1); - - buffer.flip(); - var deserialized = handler.deserialize(buffer); - assertThat(deserialized).isEqualTo(value); - assertThat(((Value.BoolValue) deserialized).getValue()).isEqualTo(testValue); - } - - @ParameterizedTest - @ValueSource(ints = {0, 1, -1, Integer.MAX_VALUE, Integer.MIN_VALUE, 42, -42}) - void testInt32Handler(int testValue) throws ImprintException { - var handler = TypeHandler.INT32; - var value = Value.fromInt32(testValue); - - // Size estimation - assertThat(handler.estimateSize(value)).isEqualTo(4); - - // Round-trip test - var buffer = ByteBuffer.allocate(10).order(ByteOrder.LITTLE_ENDIAN); - handler.serialize(value, buffer); - assertThat(buffer.position()).isEqualTo(4); - - buffer.flip(); - var deserialized = handler.deserialize(buffer); - assertThat(deserialized).isEqualTo(value); - assertThat(((Value.Int32Value) deserialized).getValue()).isEqualTo(testValue); - } - - @ParameterizedTest - @ValueSource(longs = {0L, 1L, -1L, Long.MAX_VALUE, Long.MIN_VALUE, 123456789L}) - void testInt64Handler(long testValue) throws ImprintException { - var handler = TypeHandler.INT64; - var value = Value.fromInt64(testValue); - - // Size estimation - assertThat(handler.estimateSize(value)).isEqualTo(8); - - // Round-trip test - var buffer = ByteBuffer.allocate(20).order(ByteOrder.LITTLE_ENDIAN); - handler.serialize(value, buffer); - assertThat(buffer.position()).isEqualTo(8); - - buffer.flip(); - var deserialized = handler.deserialize(buffer); - assertThat(deserialized).isEqualTo(value); - assertThat(((Value.Int64Value) deserialized).getValue()).isEqualTo(testValue); - } - - @ParameterizedTest - @ValueSource(floats = {0.0f, 1.0f, -1.0f, Float.MAX_VALUE, Float.MIN_VALUE, 3.14159f, Float.NaN, Float.POSITIVE_INFINITY}) - void testFloat32Handler(float testValue) throws ImprintException { - var handler = TypeHandler.FLOAT32; - var value = Value.fromFloat32(testValue); - - // Size estimation - assertThat(handler.estimateSize(value)).isEqualTo(4); - - // Round-trip test - var buffer = ByteBuffer.allocate(10).order(ByteOrder.LITTLE_ENDIAN); - handler.serialize(value, buffer); - assertThat(buffer.position()).isEqualTo(4); - - buffer.flip(); - var deserialized = handler.deserialize(buffer); - assertThat(deserialized).isEqualTo(value); - - float deserializedValue = ((Value.Float32Value) deserialized).getValue(); - if (Float.isNaN(testValue)) { - assertThat(deserializedValue).isNaN(); - } else { - assertThat(deserializedValue).isEqualTo(testValue); - } - } - - @ParameterizedTest - @ValueSource(doubles = {0.0, 1.0, -1.0, Double.MAX_VALUE, Double.MIN_VALUE, Math.PI, Double.NaN, Double.POSITIVE_INFINITY}) - void testFloat64Handler(double testValue) throws ImprintException { - var handler = TypeHandler.FLOAT64; - var value = Value.fromFloat64(testValue); - - // Size estimation - assertThat(handler.estimateSize(value)).isEqualTo(8); - - // Round-trip test - var buffer = ByteBuffer.allocate(20).order(ByteOrder.LITTLE_ENDIAN); - handler.serialize(value, buffer); - assertThat(buffer.position()).isEqualTo(8); - - buffer.flip(); - var deserialized = handler.deserialize(buffer); - assertThat(deserialized).isEqualTo(value); - - double deserializedValue = ((Value.Float64Value) deserialized).getValue(); - if (Double.isNaN(testValue)) { - assertThat(deserializedValue).isNaN(); - } else { - assertThat(deserializedValue).isEqualTo(testValue); - } - } - - @ParameterizedTest - @ValueSource(strings = {"", "hello", "世界", "a very long string that exceeds typical buffer sizes and contains unicode: 🚀🎉", "null\0bytes"}) - void testStringHandler(String testValue) throws ImprintException { - var handler = TypeHandler.STRING; - var value = Value.fromString(testValue); - - byte[] utf8Bytes = testValue.getBytes(java.nio.charset.StandardCharsets.UTF_8); - int expectedSize = com.imprint.util.VarInt.encodedLength(utf8Bytes.length) + utf8Bytes.length; - - // Size estimation - assertThat(handler.estimateSize(value)).isEqualTo(expectedSize); - - // Round-trip test - var buffer = ByteBuffer.allocate(expectedSize + 20).order(ByteOrder.LITTLE_ENDIAN); - handler.serialize(value, buffer); - - buffer.flip(); - var deserialized = handler.deserialize(buffer); - - // Should return StringBufferValue (zero-copy implementation) - assertThat(deserialized).isInstanceOf(Value.StringBufferValue.class); - - String deserializedString; - if (deserialized instanceof Value.StringBufferValue) { - deserializedString = ((Value.StringBufferValue) deserialized).getValue(); - } else { - deserializedString = ((Value.StringValue) deserialized).getValue(); - } - - assertThat(deserializedString).isEqualTo(testValue); - } - - @Test - void testBytesHandlerWithArrayValue() throws ImprintException { - var handler = TypeHandler.BYTES; - byte[] testBytes = {0, 1, 2, (byte) 0xFF, 42, 127, -128}; - var value = Value.fromBytes(testBytes); - - int expectedSize = com.imprint.util.VarInt.encodedLength(testBytes.length) + testBytes.length; - - // Size estimation - assertThat(handler.estimateSize(value)).isEqualTo(expectedSize); - - // Round-trip test - var buffer = ByteBuffer.allocate(expectedSize + 20).order(ByteOrder.LITTLE_ENDIAN); - handler.serialize(value, buffer); - - buffer.flip(); - var deserialized = handler.deserialize(buffer); - - // Should return BytesBufferValue (zero-copy implementation) - assertThat(deserialized).isInstanceOf(Value.BytesBufferValue.class); - - byte[] deserializedBytes = ((Value.BytesBufferValue) deserialized).getValue(); - assertThat(deserializedBytes).isEqualTo(testBytes); - } - - @Test - void testBytesHandlerWithBufferValue() throws ImprintException { - var handler = TypeHandler.BYTES; - byte[] testBytes = {10, 20, 30, 40}; - var bufferValue = Value.fromBytesBuffer(ByteBuffer.wrap(testBytes).asReadOnlyBuffer()); - - int expectedSize = com.imprint.util.VarInt.encodedLength(testBytes.length) + testBytes.length; - - // Size estimation - assertThat(handler.estimateSize(bufferValue)).isEqualTo(expectedSize); - - // Round-trip test - var buffer = ByteBuffer.allocate(expectedSize + 20).order(ByteOrder.LITTLE_ENDIAN); - handler.serialize(bufferValue, buffer); - - buffer.flip(); - var deserialized = handler.deserialize(buffer); - - byte[] deserializedBytes = ((Value.BytesBufferValue) deserialized).getValue(); - assertThat(deserializedBytes).isEqualTo(testBytes); - } - - @Test - void testStringHandlerWithBufferValue() throws ImprintException { - var handler = TypeHandler.STRING; - String testString = "zero-copy string test"; - byte[] utf8Bytes = testString.getBytes(java.nio.charset.StandardCharsets.UTF_8); - var bufferValue = Value.fromStringBuffer(ByteBuffer.wrap(utf8Bytes).asReadOnlyBuffer()); - - int expectedSize = com.imprint.util.VarInt.encodedLength(utf8Bytes.length) + utf8Bytes.length; - - // Size estimation - assertThat(handler.estimateSize(bufferValue)).isEqualTo(expectedSize); - - // Round-trip test - var buffer = ByteBuffer.allocate(expectedSize + 20).order(ByteOrder.LITTLE_ENDIAN); - handler.serialize(bufferValue, buffer); - - buffer.flip(); - var deserialized = handler.deserialize(buffer); - - String deserializedString = ((Value.StringBufferValue) deserialized).getValue(); - assertThat(deserializedString).isEqualTo(testString); - } - - @Test - void testBoolHandlerInvalidValue() { - var handler = TypeHandler.BOOL; - var buffer = ByteBuffer.allocate(10); - buffer.put((byte) 2); // Invalid boolean value - buffer.flip(); - - assertThatThrownBy(() -> handler.deserialize(buffer)) - .isInstanceOf(ImprintException.class) - .hasMessageContaining("Invalid boolean value: 2"); - } - - @Test - void testHandlerBufferUnderflow() { - // Test that handlers properly detect buffer underflow - var int32Handler = TypeHandler.INT32; - var buffer = ByteBuffer.allocate(2); // Too small for int32 - - assertThatThrownBy(() -> int32Handler.deserialize(buffer)) - .isInstanceOf(ImprintException.class) - .hasMessageContaining("Not enough bytes for int32"); - } -} \ No newline at end of file diff --git a/src/test/java/com/imprint/types/ValueTest.java b/src/test/java/com/imprint/types/ValueTest.java deleted file mode 100644 index b092bb7..0000000 --- a/src/test/java/com/imprint/types/ValueTest.java +++ /dev/null @@ -1,218 +0,0 @@ -package com.imprint.types; - -import org.junit.jupiter.api.Test; - -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class ValueTest { - - @Test - void shouldCreateNullValue() { - Value value = Value.nullValue(); - - assertThat(value).isInstanceOf(Value.NullValue.class); - assertThat(value.getTypeCode()).isEqualTo(TypeCode.NULL); - assertThat(value.toString()).isEqualTo("null"); - } - - @Test - void shouldCreateBooleanValues() { - Value trueValue = Value.fromBoolean(true); - Value falseValue = Value.fromBoolean(false); - - assertThat(trueValue).isInstanceOf(Value.BoolValue.class); - assertThat(((Value.BoolValue) trueValue).getValue()).isTrue(); - assertThat(trueValue.getTypeCode()).isEqualTo(TypeCode.BOOL); - - assertThat(falseValue).isInstanceOf(Value.BoolValue.class); - assertThat(((Value.BoolValue) falseValue).getValue()).isFalse(); - assertThat(falseValue.getTypeCode()).isEqualTo(TypeCode.BOOL); - } - - @Test - void shouldCreateNumericValues() { - var int32 = Value.fromInt32(42); - var int64 = Value.fromInt64(123456789L); - var float32 = Value.fromFloat32(3.14f); - var float64 = Value.fromFloat64(2.718281828); - - assertThat(int32.getTypeCode()).isEqualTo(TypeCode.INT32); - assertThat(((Value.Int32Value) int32).getValue()).isEqualTo(42); - - assertThat(int64.getTypeCode()).isEqualTo(TypeCode.INT64); - assertThat(((Value.Int64Value) int64).getValue()).isEqualTo(123456789L); - - assertThat(float32.getTypeCode()).isEqualTo(TypeCode.FLOAT32); - assertThat(((Value.Float32Value) float32).getValue()).isEqualTo(3.14f); - - assertThat(float64.getTypeCode()).isEqualTo(TypeCode.FLOAT64); - assertThat(((Value.Float64Value) float64).getValue()).isEqualTo(2.718281828); - } - - @Test - void shouldCreateBytesAndStringValues() { - byte[] bytes = {1, 2, 3, 4}; - var bytesValue = Value.fromBytes(bytes); - var stringValue = Value.fromString("hello"); - - assertThat(bytesValue.getTypeCode()).isEqualTo(TypeCode.BYTES); - assertThat(((Value.BytesValue) bytesValue).getValue()).isEqualTo(bytes); - - assertThat(stringValue.getTypeCode()).isEqualTo(TypeCode.STRING); - assertThat(((Value.StringValue) stringValue).getValue()).isEqualTo("hello"); - } - - @Test - void shouldCreateArrayValues() { - List elements = Arrays.asList( - Value.fromInt32(1), - Value.fromInt32(2), - Value.fromInt32(3) - ); - Value arrayValue = Value.fromArray(elements); - - assertThat(arrayValue.getTypeCode()).isEqualTo(TypeCode.ARRAY); - assertThat(((Value.ArrayValue) arrayValue).getValue()).isEqualTo(elements); - } - - @Test - void shouldCreateMapValues() { - var map = new HashMap(); - map.put(MapKey.fromString("key1"), Value.fromInt32(1)); - map.put(MapKey.fromString("key2"), Value.fromInt32(2)); - - Value mapValue = Value.fromMap(map); - - assertThat(mapValue.getTypeCode()).isEqualTo(TypeCode.MAP); - assertThat(((Value.MapValue) mapValue).getValue()).isEqualTo(map); - } - - @Test - void shouldHandleEqualityCorrectly() { - var int1 = Value.fromInt32(42); - var int2 = Value.fromInt32(42); - var int3 = Value.fromInt32(43); - - assertThat(int1).isEqualTo(int2); - assertThat(int1).isNotEqualTo(int3); - assertThat(int1.hashCode()).isEqualTo(int2.hashCode()); - } - - @Test - void shouldRejectNullString() { - assertThatThrownBy(() -> Value.fromString(null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - void shouldCreateStringBufferValue() { - String testString = "hello world"; - byte[] utf8Bytes = testString.getBytes(StandardCharsets.UTF_8); - ByteBuffer buffer = ByteBuffer.wrap(utf8Bytes); - - Value stringBufferValue = Value.fromStringBuffer(buffer); - - assertThat(stringBufferValue).isInstanceOf(Value.StringBufferValue.class); - assertThat(stringBufferValue.getTypeCode()).isEqualTo(TypeCode.STRING); - assertThat(((Value.StringBufferValue) stringBufferValue).getValue()).isEqualTo(testString); - } - - @Test - void shouldCreateBytesBufferValue() { - byte[] testBytes = {1, 2, 3, 4, 5}; - ByteBuffer buffer = ByteBuffer.wrap(testBytes); - - Value bytesBufferValue = Value.fromBytesBuffer(buffer); - - assertThat(bytesBufferValue).isInstanceOf(Value.BytesBufferValue.class); - assertThat(bytesBufferValue.getTypeCode()).isEqualTo(TypeCode.BYTES); - assertThat(((Value.BytesBufferValue) bytesBufferValue).getValue()).isEqualTo(testBytes); - } - - @Test - void shouldHandleStringBufferValueFastPath() { - // Array-backed buffer with arrayOffset() == 0 should use fast path - String testString = "fast path test"; - byte[] utf8Bytes = testString.getBytes(StandardCharsets.UTF_8); - ByteBuffer buffer = ByteBuffer.wrap(utf8Bytes); - - Value stringBufferValue = Value.fromStringBuffer(buffer); - - // Should work correctly regardless of path taken - assertThat(((Value.StringBufferValue) stringBufferValue).getValue()).isEqualTo(testString); - } - - @Test - void shouldHandleStringBufferValueFallbackPath() { - // Sliced buffer will have non-zero arrayOffset, forcing fallback path - String testString = "fallback path test"; - byte[] utf8Bytes = testString.getBytes(StandardCharsets.UTF_8); - ByteBuffer buffer = ByteBuffer.wrap(utf8Bytes); - ByteBuffer sliced = buffer.slice(); // This may break arrayOffset() == 0 - - Value stringBufferValue = Value.fromStringBuffer(sliced); - - // Should work correctly regardless of path taken - assertThat(((Value.StringBufferValue) stringBufferValue).getValue()).isEqualTo(testString); - } - - @Test - void shouldHandleLargeStringWithoutCaching() { - // Create string > 1KB to test the no-cache path - String largeString = "x".repeat(2000); - byte[] utf8Bytes = largeString.getBytes(StandardCharsets.UTF_8); - ByteBuffer buffer = ByteBuffer.wrap(utf8Bytes).slice(); // Force fallback path - - Value stringBufferValue = Value.fromStringBuffer(buffer); - - assertThat(((Value.StringBufferValue) stringBufferValue).getValue()).isEqualTo(largeString); - } - - @Test - void shouldCacheStringDecoding() { - String testString = "cache test"; - byte[] utf8Bytes = testString.getBytes(StandardCharsets.UTF_8); - ByteBuffer buffer = ByteBuffer.wrap(utf8Bytes); - - Value.StringBufferValue stringBufferValue = (Value.StringBufferValue) Value.fromStringBuffer(buffer); - - // First call should decode and cache - String result1 = stringBufferValue.getValue(); - // Second call should return cached value - String result2 = stringBufferValue.getValue(); - - assertThat(result1).isEqualTo(testString); - assertThat(result2).isEqualTo(testString); - assertThat(result1).isSameAs(result2); // Should be same object reference due to caching - } - - @Test - void shouldHandleStringValueEquality() { - String testString = "equality test"; - - Value stringValue = Value.fromString(testString); - Value stringBufferValue = Value.fromStringBuffer(ByteBuffer.wrap(testString.getBytes(StandardCharsets.UTF_8))); - - assertThat(stringValue).isEqualTo(stringBufferValue); - assertThat(stringBufferValue).isEqualTo(stringValue); - assertThat(stringValue.hashCode()).isEqualTo(stringBufferValue.hashCode()); - } - - @Test - void shouldHandleBytesValueEquality() { - byte[] testBytes = {1, 2, 3, 4, 5}; - - Value bytesValue = Value.fromBytes(testBytes); - Value bytesBufferValue = Value.fromBytesBuffer(ByteBuffer.wrap(testBytes)); - - assertThat(bytesValue).isEqualTo(bytesBufferValue); - assertThat(bytesBufferValue).isEqualTo(bytesValue); - } -} \ No newline at end of file