Skip to content

Commit 5f9c58d

Browse files
lavrukovAlexander Lavrukov
andauthored
ByteArray class for primary keys with byte[] (#17)
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]>
1 parent 112f9d6 commit 5f9c58d

File tree

15 files changed

+308
-8
lines changed

15 files changed

+308
-8
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package tech.ydb.yoj.databind;
2+
3+
import org.jetbrains.annotations.NotNull;
4+
5+
import java.util.Arrays;
6+
7+
public final class ByteArray implements Comparable<ByteArray> {
8+
private static final char[] HEX_CHARS = "0123456789abcdef".toCharArray();
9+
10+
private final byte[] array;
11+
12+
private ByteArray(byte[] array) {
13+
this.array = array;
14+
}
15+
16+
public static ByteArray wrap(byte[] array) {
17+
return new ByteArray(array);
18+
}
19+
20+
public static ByteArray copy(byte[] array) {
21+
return new ByteArray(array.clone());
22+
}
23+
24+
public ByteArray copy() {
25+
return ByteArray.copy(array);
26+
}
27+
28+
public byte[] getArray() {
29+
return array;
30+
}
31+
32+
@Override
33+
public boolean equals(Object o) {
34+
if (this == o) {
35+
return true;
36+
}
37+
if (o == null || getClass() != o.getClass()) {
38+
return false;
39+
}
40+
ByteArray that = (ByteArray) o;
41+
return Arrays.equals(array, that.array);
42+
}
43+
44+
@Override
45+
public int hashCode() {
46+
return Arrays.hashCode(array);
47+
}
48+
49+
@Override
50+
public int compareTo(@NotNull ByteArray o) {
51+
return Arrays.compare(array, o.array);
52+
}
53+
54+
@Override
55+
public String toString() {
56+
if (array.length > 16) {
57+
return "bytes(length > 16)";
58+
}
59+
60+
char[] hexChars = new char[array.length * 2 + 7];
61+
hexChars[0] = 'b';
62+
hexChars[1] = 'y';
63+
hexChars[2] = 't';
64+
hexChars[3] = 'e';
65+
hexChars[4] = 's';
66+
hexChars[5] = '(';
67+
68+
int i = 0;
69+
for (; i < array.length; i++) {
70+
int v = array[i] & 0xFF;
71+
int ci = i * 2 + 6;
72+
hexChars[ci] = HEX_CHARS[v >>> 4];
73+
hexChars[ci + 1] = HEX_CHARS[v & 0x0F];
74+
}
75+
hexChars[i * 2 + 6] = ')';
76+
77+
return new String(hexChars);
78+
}
79+
}

databind/src/main/java/tech/ydb/yoj/databind/FieldValueType.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ public enum FieldValueType {
6262
* Java-side <strong>must</strong> be a byte array.
6363
*/
6464
BINARY,
65+
/**
66+
* Binary value: just a stream of uninterpreted bytes.
67+
* Java-side <strong>must</strong> be a {@link ByteArray tech.ydb.yoj.databind.ByteArray}.
68+
*/
69+
BYTE_ARRAY,
6570
/**
6671
* Composite value. Can contain any other values, including other composite values.<br>
6772
* Java-side must be an immutable POJO with all-args constructor, e.g. a Lombok {@code @Value}-annotated
@@ -80,7 +85,7 @@ public enum FieldValueType {
8085
UNKNOWN;
8186

8287
private static final Set<FieldValueType> SORTABLE_VALUE_TYPES = Set.of(
83-
INTEGER, STRING, ENUM, TIMESTAMP
88+
INTEGER, STRING, ENUM, TIMESTAMP, BYTE_ARRAY
8489
);
8590

8691
private static final Set<Type> INTEGER_NUMERIC_TYPES = Set.of(
@@ -152,6 +157,8 @@ public static FieldValueType forJavaType(@NonNull Type type) {
152157
return INTERVAL;
153158
} else if (byte[].class.equals(type)) {
154159
return BINARY;
160+
} else if (ByteArray.class.equals(type)) {
161+
return BYTE_ARRAY;
155162
} else if (Collection.class.isAssignableFrom(clazz)) {
156163
throw new IllegalArgumentException("Raw collection types cannot be used in databinding: " + type);
157164
} else if (Object.class.equals(clazz)) {

databind/src/main/java/tech/ydb/yoj/databind/expression/FieldValue.java

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import lombok.NonNull;
77
import lombok.RequiredArgsConstructor;
88
import lombok.Value;
9+
import tech.ydb.yoj.databind.ByteArray;
910
import tech.ydb.yoj.databind.FieldValueType;
1011
import tech.ydb.yoj.databind.schema.ObjectSchema;
1112
import tech.ydb.yoj.databind.schema.Schema.JavaField;
@@ -34,35 +35,41 @@ public class FieldValue {
3435
Boolean bool;
3536
Instant timestamp;
3637
Tuple tuple;
38+
ByteArray byteArray;
3739

3840
@NonNull
3941
public static FieldValue ofStr(@NonNull String str) {
40-
return new FieldValue(str, null, null, null, null, null);
42+
return new FieldValue(str, null, null, null, null, null, null);
4143
}
4244

4345
@NonNull
4446
public static FieldValue ofNum(long num) {
45-
return new FieldValue(null, num, null, null, null, null);
47+
return new FieldValue(null, num, null, null, null, null, null);
4648
}
4749

4850
@NonNull
4951
public static FieldValue ofReal(double real) {
50-
return new FieldValue(null, null, real, null, null, null);
52+
return new FieldValue(null, null, real, null, null, null, null);
5153
}
5254

5355
@NonNull
5456
public static FieldValue ofBool(boolean bool) {
55-
return new FieldValue(null, null, null, bool, null, null);
57+
return new FieldValue(null, null, null, bool, null, null, null);
5658
}
5759

5860
@NonNull
5961
public static FieldValue ofTimestamp(@NonNull Instant timestamp) {
60-
return new FieldValue(null, null, null, null, timestamp, null);
62+
return new FieldValue(null, null, null, null, timestamp, null, null);
6163
}
6264

6365
@NonNull
6466
public static FieldValue ofTuple(@NonNull Tuple tuple) {
65-
return new FieldValue(null, null, null, null, null, tuple);
67+
return new FieldValue(null, null, null, null, null, tuple, null);
68+
}
69+
70+
@NonNull
71+
public static FieldValue ofByteArray(@NonNull ByteArray byteArray) {
72+
return new FieldValue(null, null, null, null, null, null, byteArray);
6673
}
6774

6875
@NonNull
@@ -84,6 +91,9 @@ public static FieldValue ofObj(@NonNull Object obj) {
8491
case BOOLEAN -> {
8592
return ofBool((Boolean) obj);
8693
}
94+
case BYTE_ARRAY -> {
95+
return ofByteArray((ByteArray) obj);
96+
}
8797
case TIMESTAMP -> {
8898
return ofTimestamp((Instant) obj);
8999
}
@@ -133,6 +143,10 @@ public boolean isTuple() {
133143
return tuple != null;
134144
}
135145

146+
public boolean isByteArray() {
147+
return byteArray != null;
148+
}
149+
136150
@Nullable
137151
public static Comparable<?> getComparable(@NonNull Map<String, Object> values,
138152
@NonNull JavaField field) {
@@ -201,6 +215,10 @@ public Comparable<?> getComparable(@NonNull Type fieldType) {
201215
Preconditions.checkState(isBool(), "Value is not a boolean: %s", this);
202216
return bool;
203217
}
218+
case BYTE_ARRAY -> {
219+
Preconditions.checkState(isByteArray(), "Value is not a ByteArray: %s", this);
220+
return byteArray;
221+
}
204222
case COMPOSITE -> {
205223
Preconditions.checkState(isTuple(), "Value is not a tuple: %s", this);
206224
Preconditions.checkState(tuple.getType().equals(fieldType),

databind/src/main/java/tech/ydb/yoj/databind/expression/IllegalExpressionException.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ static final class BooleanFieldExpected extends FieldTypeError {
5656
}
5757
}
5858

59+
static final class ByteArrayFieldExpected extends FieldTypeError {
60+
ByteArrayFieldExpected(String field) {
61+
super(field, "Type mismatch: cannot compare field \"%s\" with a ByteArray value"::formatted);
62+
}
63+
}
64+
5965
static final class DateTimeFieldExpected extends FieldTypeError {
6066
DateTimeFieldExpected(String field) {
6167
super(field, "Type mismatch: cannot compare field \"%s\" with a date-time value"::formatted);

databind/src/main/java/tech/ydb/yoj/databind/expression/ModelField.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import lombok.NonNull;
66
import tech.ydb.yoj.databind.FieldValueType;
77
import tech.ydb.yoj.databind.expression.IllegalExpressionException.FieldTypeError.BooleanFieldExpected;
8+
import tech.ydb.yoj.databind.expression.IllegalExpressionException.FieldTypeError.ByteArrayFieldExpected;
89
import tech.ydb.yoj.databind.expression.IllegalExpressionException.FieldTypeError.DateTimeFieldExpected;
910
import tech.ydb.yoj.databind.expression.IllegalExpressionException.FieldTypeError.FlatFieldExpected;
1011
import tech.ydb.yoj.databind.expression.IllegalExpressionException.FieldTypeError.IntegerFieldExpected;
@@ -96,6 +97,10 @@ public FieldValue validateValue(@NonNull FieldValue value) {
9697
checkArgument(fieldValueType == FieldValueType.BOOLEAN,
9798
BooleanFieldExpected::new,
9899
p -> format("Specified a boolean value for non-boolean field \"%s\"", p));
100+
} else if (value.isByteArray()) {
101+
checkArgument(fieldValueType == FieldValueType.BYTE_ARRAY,
102+
ByteArrayFieldExpected::new,
103+
p -> format("Specified a ByteArray value for non-ByteArray field \"%s\"", p));
99104
} else if (value.isTimestamp()) {
100105
checkArgument(fieldValueType == FieldValueType.TIMESTAMP || fieldValueType == FieldValueType.INTEGER,
101106
DateTimeFieldExpected::new,
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package tech.ydb.yoj.databind;
2+
3+
import org.junit.Assert;
4+
import org.junit.Test;
5+
6+
import java.util.List;
7+
import java.util.TreeSet;
8+
9+
public class ByteArrayTest {
10+
@Test
11+
public void testWrap() {
12+
byte[] arr = bytesOf(0, 1, 2);
13+
var b = ByteArray.wrap(arr);
14+
b.getArray()[1] = 3;
15+
16+
Assert.assertEquals(arr[1], 3);
17+
}
18+
19+
@Test
20+
public void testCopy() {
21+
byte[] arr = bytesOf(0, 1, 2);
22+
var b = ByteArray.copy(arr);
23+
b.getArray()[1] = 3;
24+
25+
Assert.assertEquals(arr[1], 1);
26+
}
27+
28+
@Test
29+
public void testEquals() {
30+
byte[] arrA = bytesOf(0, 1, 2);
31+
byte[] arrB = bytesOf(0, 1, 2);
32+
var a = ByteArray.wrap(arrA);
33+
var b = ByteArray.wrap(arrB);
34+
35+
Assert.assertNotEquals(arrA, arrB);
36+
Assert.assertEquals(a, a);
37+
Assert.assertEquals(a, b);
38+
Assert.assertNotEquals(a, null);
39+
}
40+
41+
@Test
42+
public void testSetWork() {
43+
TreeSet<ByteArray> set = new TreeSet<>(List.of(
44+
valueOf(0, 1, 2),
45+
valueOf(0, 1, 2),
46+
valueOf(),
47+
valueOf(255),
48+
valueOf(1, 2, 3),
49+
valueOf(1, 2, 3),
50+
valueOf(0, 1, 3)
51+
));
52+
53+
Assert.assertEquals(set.stream().toList(), List.of(
54+
valueOf(),
55+
valueOf(255),
56+
valueOf(0, 1, 2),
57+
valueOf(0, 1, 3),
58+
valueOf(1, 2, 3)
59+
));
60+
}
61+
62+
@Test
63+
public void testCompare() {
64+
Assert.assertEquals(0, valueOf(0, 1, 2).compareTo(valueOf(0, 1, 2)));
65+
Assert.assertEquals(1, valueOf(0, 1, 3).compareTo(valueOf(0, 1, 2)));
66+
Assert.assertTrue(0 > valueOf(0, 1, 2).compareTo(valueOf(1, 2, 3)));
67+
Assert.assertTrue(0 > valueOf().compareTo(valueOf(1, 2, 3)));
68+
Assert.assertTrue(0 < valueOf(1, 2, 3).compareTo(valueOf()));
69+
Assert.assertTrue(0 < valueOf(0, 1, 2).compareTo(valueOf(0, 1)));
70+
}
71+
72+
@Test
73+
public void testToString() {
74+
Assert.assertEquals("bytes()", valueOf().toString());
75+
Assert.assertEquals("bytes(00)", valueOf(0).toString());
76+
Assert.assertEquals("bytes(0102ff)", valueOf(1, 2, 255).toString());
77+
Assert.assertEquals("bytes(00a2fe00)", valueOf(0, 162, 254, 0).toString());
78+
Assert.assertEquals(
79+
"bytes(01010101010101010101010101010101)",
80+
valueOf(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1).toString()
81+
);
82+
Assert.assertEquals(
83+
"bytes(length > 16)",
84+
valueOf(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1).toString()
85+
);
86+
}
87+
88+
private static byte[] bytesOf(int... array) {
89+
byte[] result = new byte[array.length];
90+
for (int i = 0; i < array.length; i++) {
91+
result[i] = (byte) array[i];
92+
}
93+
return result;
94+
}
95+
96+
private static ByteArray valueOf(int... array) {
97+
byte[] result = new byte[array.length];
98+
for (int i = 0; i < array.length; i++) {
99+
result[i] = (byte) array[i];
100+
}
101+
return ByteArray.wrap(result);
102+
}
103+
}

repository-inmemory/src/main/java/tech/ydb/yoj/repository/test/inmemory/Columns.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.eclipse.collections.api.map.ImmutableMap;
77
import org.eclipse.collections.api.tuple.Pair;
88
import org.eclipse.collections.impl.factory.Maps;
9+
import tech.ydb.yoj.databind.ByteArray;
910
import tech.ydb.yoj.databind.schema.Schema;
1011
import tech.ydb.yoj.repository.DbTypeQualifier;
1112
import tech.ydb.yoj.repository.db.Entity;
@@ -77,6 +78,7 @@ private static Object serialize(Schema.JavaField field, Object value) {
7778
: CommonConverters.serializeEnumValue(type, value);
7879
case OBJECT -> CommonConverters.serializeOpaqueObjectValue(type, value);
7980
case BINARY -> ((byte[]) value).clone();
81+
case BYTE_ARRAY -> ((ByteArray) value).copy().getArray();
8082
case BOOLEAN, INTEGER, REAL -> value;
8183
// TODO: Unify Instant and Duration handling in InMemory and YDB Repository
8284
case INTERVAL, TIMESTAMP -> value;
@@ -102,6 +104,7 @@ private static Object deserialize(Schema.JavaField field, Object value) {
102104
: CommonConverters.deserializeEnumValue(type, value);
103105
case OBJECT -> CommonConverters.deserializeOpaqueObjectValue(type, value);
104106
case BINARY -> ((byte[]) value).clone();
107+
case BYTE_ARRAY -> ByteArray.copy((byte[]) value);
105108
case BOOLEAN, INTEGER, REAL -> value;
106109
// TODO: Unify Instant and Duration handling in InMemory and YDB Repository
107110
case INTERVAL, TIMESTAMP -> value;

0 commit comments

Comments
 (0)