Skip to content

Commit

Permalink
ByteArray class for primary keys with byte[] (#17)
Browse files Browse the repository at this point in the history
Byte arrays in primary keys support.

We can't do it with simple `byte[]` because Java Records' default `equals()`
compares references, not real array data. Special class for byte arrays
looks better than magic over `byte[]`, anyway.

---------

Co-authored-by: Alexander Lavrukov <[email protected]>
  • Loading branch information
lavrukov and Alexander Lavrukov authored Feb 14, 2024
1 parent 112f9d6 commit 5f9c58d
Show file tree
Hide file tree
Showing 15 changed files with 308 additions and 8 deletions.
79 changes: 79 additions & 0 deletions databind/src/main/java/tech/ydb/yoj/databind/ByteArray.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package tech.ydb.yoj.databind;

import org.jetbrains.annotations.NotNull;

import java.util.Arrays;

public final class ByteArray implements Comparable<ByteArray> {
private static final char[] HEX_CHARS = "0123456789abcdef".toCharArray();

private final byte[] array;

private ByteArray(byte[] array) {
this.array = array;
}

public static ByteArray wrap(byte[] array) {
return new ByteArray(array);
}

public static ByteArray copy(byte[] array) {
return new ByteArray(array.clone());
}

public ByteArray copy() {
return ByteArray.copy(array);
}

public byte[] getArray() {
return array;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ByteArray that = (ByteArray) o;
return Arrays.equals(array, that.array);
}

@Override
public int hashCode() {
return Arrays.hashCode(array);
}

@Override
public int compareTo(@NotNull ByteArray o) {
return Arrays.compare(array, o.array);
}

@Override
public String toString() {
if (array.length > 16) {
return "bytes(length > 16)";
}

char[] hexChars = new char[array.length * 2 + 7];
hexChars[0] = 'b';
hexChars[1] = 'y';
hexChars[2] = 't';
hexChars[3] = 'e';
hexChars[4] = 's';
hexChars[5] = '(';

int i = 0;
for (; i < array.length; i++) {
int v = array[i] & 0xFF;
int ci = i * 2 + 6;
hexChars[ci] = HEX_CHARS[v >>> 4];
hexChars[ci + 1] = HEX_CHARS[v & 0x0F];
}
hexChars[i * 2 + 6] = ')';

return new String(hexChars);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ public enum FieldValueType {
* Java-side <strong>must</strong> be a byte array.
*/
BINARY,
/**
* Binary value: just a stream of uninterpreted bytes.
* Java-side <strong>must</strong> be a {@link ByteArray tech.ydb.yoj.databind.ByteArray}.
*/
BYTE_ARRAY,
/**
* Composite value. Can contain any other values, including other composite values.<br>
* Java-side must be an immutable POJO with all-args constructor, e.g. a Lombok {@code @Value}-annotated
Expand All @@ -80,7 +85,7 @@ public enum FieldValueType {
UNKNOWN;

private static final Set<FieldValueType> SORTABLE_VALUE_TYPES = Set.of(
INTEGER, STRING, ENUM, TIMESTAMP
INTEGER, STRING, ENUM, TIMESTAMP, BYTE_ARRAY
);

private static final Set<Type> INTEGER_NUMERIC_TYPES = Set.of(
Expand Down Expand Up @@ -152,6 +157,8 @@ public static FieldValueType forJavaType(@NonNull Type type) {
return INTERVAL;
} else if (byte[].class.equals(type)) {
return BINARY;
} else if (ByteArray.class.equals(type)) {
return BYTE_ARRAY;
} else if (Collection.class.isAssignableFrom(clazz)) {
throw new IllegalArgumentException("Raw collection types cannot be used in databinding: " + type);
} else if (Object.class.equals(clazz)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import tech.ydb.yoj.databind.ByteArray;
import tech.ydb.yoj.databind.FieldValueType;
import tech.ydb.yoj.databind.schema.ObjectSchema;
import tech.ydb.yoj.databind.schema.Schema.JavaField;
Expand Down Expand Up @@ -34,35 +35,41 @@ public class FieldValue {
Boolean bool;
Instant timestamp;
Tuple tuple;
ByteArray byteArray;

@NonNull
public static FieldValue ofStr(@NonNull String str) {
return new FieldValue(str, null, null, null, null, null);
return new FieldValue(str, null, null, null, null, null, null);
}

@NonNull
public static FieldValue ofNum(long num) {
return new FieldValue(null, num, null, null, null, null);
return new FieldValue(null, num, null, null, null, null, null);
}

@NonNull
public static FieldValue ofReal(double real) {
return new FieldValue(null, null, real, null, null, null);
return new FieldValue(null, null, real, null, null, null, null);
}

@NonNull
public static FieldValue ofBool(boolean bool) {
return new FieldValue(null, null, null, bool, null, null);
return new FieldValue(null, null, null, bool, null, null, null);
}

@NonNull
public static FieldValue ofTimestamp(@NonNull Instant timestamp) {
return new FieldValue(null, null, null, null, timestamp, null);
return new FieldValue(null, null, null, null, timestamp, null, null);
}

@NonNull
public static FieldValue ofTuple(@NonNull Tuple tuple) {
return new FieldValue(null, null, null, null, null, tuple);
return new FieldValue(null, null, null, null, null, tuple, null);
}

@NonNull
public static FieldValue ofByteArray(@NonNull ByteArray byteArray) {
return new FieldValue(null, null, null, null, null, null, byteArray);
}

@NonNull
Expand All @@ -84,6 +91,9 @@ public static FieldValue ofObj(@NonNull Object obj) {
case BOOLEAN -> {
return ofBool((Boolean) obj);
}
case BYTE_ARRAY -> {
return ofByteArray((ByteArray) obj);
}
case TIMESTAMP -> {
return ofTimestamp((Instant) obj);
}
Expand Down Expand Up @@ -133,6 +143,10 @@ public boolean isTuple() {
return tuple != null;
}

public boolean isByteArray() {
return byteArray != null;
}

@Nullable
public static Comparable<?> getComparable(@NonNull Map<String, Object> values,
@NonNull JavaField field) {
Expand Down Expand Up @@ -201,6 +215,10 @@ public Comparable<?> getComparable(@NonNull Type fieldType) {
Preconditions.checkState(isBool(), "Value is not a boolean: %s", this);
return bool;
}
case BYTE_ARRAY -> {
Preconditions.checkState(isByteArray(), "Value is not a ByteArray: %s", this);
return byteArray;
}
case COMPOSITE -> {
Preconditions.checkState(isTuple(), "Value is not a tuple: %s", this);
Preconditions.checkState(tuple.getType().equals(fieldType),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ static final class BooleanFieldExpected extends FieldTypeError {
}
}

static final class ByteArrayFieldExpected extends FieldTypeError {
ByteArrayFieldExpected(String field) {
super(field, "Type mismatch: cannot compare field \"%s\" with a ByteArray value"::formatted);
}
}

static final class DateTimeFieldExpected extends FieldTypeError {
DateTimeFieldExpected(String field) {
super(field, "Type mismatch: cannot compare field \"%s\" with a date-time value"::formatted);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import lombok.NonNull;
import tech.ydb.yoj.databind.FieldValueType;
import tech.ydb.yoj.databind.expression.IllegalExpressionException.FieldTypeError.BooleanFieldExpected;
import tech.ydb.yoj.databind.expression.IllegalExpressionException.FieldTypeError.ByteArrayFieldExpected;
import tech.ydb.yoj.databind.expression.IllegalExpressionException.FieldTypeError.DateTimeFieldExpected;
import tech.ydb.yoj.databind.expression.IllegalExpressionException.FieldTypeError.FlatFieldExpected;
import tech.ydb.yoj.databind.expression.IllegalExpressionException.FieldTypeError.IntegerFieldExpected;
Expand Down Expand Up @@ -96,6 +97,10 @@ public FieldValue validateValue(@NonNull FieldValue value) {
checkArgument(fieldValueType == FieldValueType.BOOLEAN,
BooleanFieldExpected::new,
p -> format("Specified a boolean value for non-boolean field \"%s\"", p));
} else if (value.isByteArray()) {
checkArgument(fieldValueType == FieldValueType.BYTE_ARRAY,
ByteArrayFieldExpected::new,
p -> format("Specified a ByteArray value for non-ByteArray field \"%s\"", p));
} else if (value.isTimestamp()) {
checkArgument(fieldValueType == FieldValueType.TIMESTAMP || fieldValueType == FieldValueType.INTEGER,
DateTimeFieldExpected::new,
Expand Down
103 changes: 103 additions & 0 deletions databind/src/test/java/tech/ydb/yoj/databind/ByteArrayTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package tech.ydb.yoj.databind;

import org.junit.Assert;
import org.junit.Test;

import java.util.List;
import java.util.TreeSet;

public class ByteArrayTest {
@Test
public void testWrap() {
byte[] arr = bytesOf(0, 1, 2);
var b = ByteArray.wrap(arr);
b.getArray()[1] = 3;

Assert.assertEquals(arr[1], 3);
}

@Test
public void testCopy() {
byte[] arr = bytesOf(0, 1, 2);
var b = ByteArray.copy(arr);
b.getArray()[1] = 3;

Assert.assertEquals(arr[1], 1);
}

@Test
public void testEquals() {
byte[] arrA = bytesOf(0, 1, 2);
byte[] arrB = bytesOf(0, 1, 2);
var a = ByteArray.wrap(arrA);
var b = ByteArray.wrap(arrB);

Assert.assertNotEquals(arrA, arrB);
Assert.assertEquals(a, a);
Assert.assertEquals(a, b);
Assert.assertNotEquals(a, null);
}

@Test
public void testSetWork() {
TreeSet<ByteArray> set = new TreeSet<>(List.of(
valueOf(0, 1, 2),
valueOf(0, 1, 2),
valueOf(),
valueOf(255),
valueOf(1, 2, 3),
valueOf(1, 2, 3),
valueOf(0, 1, 3)
));

Assert.assertEquals(set.stream().toList(), List.of(
valueOf(),
valueOf(255),
valueOf(0, 1, 2),
valueOf(0, 1, 3),
valueOf(1, 2, 3)
));
}

@Test
public void testCompare() {
Assert.assertEquals(0, valueOf(0, 1, 2).compareTo(valueOf(0, 1, 2)));
Assert.assertEquals(1, valueOf(0, 1, 3).compareTo(valueOf(0, 1, 2)));
Assert.assertTrue(0 > valueOf(0, 1, 2).compareTo(valueOf(1, 2, 3)));
Assert.assertTrue(0 > valueOf().compareTo(valueOf(1, 2, 3)));
Assert.assertTrue(0 < valueOf(1, 2, 3).compareTo(valueOf()));
Assert.assertTrue(0 < valueOf(0, 1, 2).compareTo(valueOf(0, 1)));
}

@Test
public void testToString() {
Assert.assertEquals("bytes()", valueOf().toString());
Assert.assertEquals("bytes(00)", valueOf(0).toString());
Assert.assertEquals("bytes(0102ff)", valueOf(1, 2, 255).toString());
Assert.assertEquals("bytes(00a2fe00)", valueOf(0, 162, 254, 0).toString());
Assert.assertEquals(
"bytes(01010101010101010101010101010101)",
valueOf(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1).toString()
);
Assert.assertEquals(
"bytes(length > 16)",
valueOf(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1).toString()
);
}

private static byte[] bytesOf(int... array) {
byte[] result = new byte[array.length];
for (int i = 0; i < array.length; i++) {
result[i] = (byte) array[i];
}
return result;
}

private static ByteArray valueOf(int... array) {
byte[] result = new byte[array.length];
for (int i = 0; i < array.length; i++) {
result[i] = (byte) array[i];
}
return ByteArray.wrap(result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.eclipse.collections.api.map.ImmutableMap;
import org.eclipse.collections.api.tuple.Pair;
import org.eclipse.collections.impl.factory.Maps;
import tech.ydb.yoj.databind.ByteArray;
import tech.ydb.yoj.databind.schema.Schema;
import tech.ydb.yoj.repository.DbTypeQualifier;
import tech.ydb.yoj.repository.db.Entity;
Expand Down Expand Up @@ -77,6 +78,7 @@ private static Object serialize(Schema.JavaField field, Object value) {
: CommonConverters.serializeEnumValue(type, value);
case OBJECT -> CommonConverters.serializeOpaqueObjectValue(type, value);
case BINARY -> ((byte[]) value).clone();
case BYTE_ARRAY -> ((ByteArray) value).copy().getArray();
case BOOLEAN, INTEGER, REAL -> value;
// TODO: Unify Instant and Duration handling in InMemory and YDB Repository
case INTERVAL, TIMESTAMP -> value;
Expand All @@ -102,6 +104,7 @@ private static Object deserialize(Schema.JavaField field, Object value) {
: CommonConverters.deserializeEnumValue(type, value);
case OBJECT -> CommonConverters.deserializeOpaqueObjectValue(type, value);
case BINARY -> ((byte[]) value).clone();
case BYTE_ARRAY -> ByteArray.copy((byte[]) value);
case BOOLEAN, INTEGER, REAL -> value;
// TODO: Unify Instant and Duration handling in InMemory and YDB Repository
case INTERVAL, TIMESTAMP -> value;
Expand Down
Loading

0 comments on commit 5f9c58d

Please sign in to comment.