From 40e8169ae0693622ce6ade49c8e0d632c4909a3e Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Fri, 6 Feb 2026 15:44:37 +0000 Subject: [PATCH 1/2] Updated PrimitiveValue.Bytes implementation --- .../tech/ydb/table/values/PrimitiveValue.java | 140 ++++++------------ .../ydb/table/values/PrimitiveValueTest.java | 59 +++++++- 2 files changed, 102 insertions(+), 97 deletions(-) diff --git a/table/src/main/java/tech/ydb/table/values/PrimitiveValue.java b/table/src/main/java/tech/ydb/table/values/PrimitiveValue.java index 923d0e251..3e900556d 100644 --- a/table/src/main/java/tech/ydb/table/values/PrimitiveValue.java +++ b/table/src/main/java/tech/ydb/table/values/PrimitiveValue.java @@ -1,6 +1,5 @@ package tech.ydb.table.values; -import java.io.Serializable; import java.nio.charset.Charset; import java.time.Duration; import java.time.Instant; @@ -10,7 +9,6 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; -import java.util.Arrays; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -20,10 +18,12 @@ import com.google.protobuf.UnsafeByteOperations; import tech.ydb.proto.ValueProtos; +import tech.ydb.table.utils.Hex; import tech.ydb.table.utils.LittleEndian; import tech.ydb.table.values.proto.ProtoValue; + /** * @author Sergey Polovko */ @@ -97,7 +97,7 @@ public String getBytesAsString(Charset charset) { } public String getText() { - throw new IllegalStateException("expected Utf8, but was " + getClass().getSimpleName()); + throw new IllegalStateException("expected Text, but was " + getClass().getSimpleName()); } public byte[] getYson() { @@ -1060,21 +1060,27 @@ public ValueProtos.Value toPb() { } private static final class Bytes extends PrimitiveValue { + private static final int OUTPUT_LIMIT = 50; private static final Bytes EMPTY_STRING = new Bytes(PrimitiveType.Bytes, new byte[0]); private static final Bytes EMPTY_YSON = new Bytes(PrimitiveType.Yson, new byte[0]); private static final long serialVersionUID = 1523630543323446576L; private final PrimitiveType type; - private final Serializable value; + private final ByteString value; + + @SuppressWarnings("VolatileArrayField") + private volatile byte[] cached; private Bytes(PrimitiveType type, byte[] value) { this.type = type; - this.value = value; + this.value = UnsafeByteOperations.unsafeWrap(value); + this.cached = value; } private Bytes(PrimitiveType type, ByteString value) { this.type = type; this.value = value; + this.cached = null; } @Override @@ -1082,34 +1088,47 @@ public PrimitiveType getType() { return type; } + private byte[] getCachedBytes() { + if (cached == null) { + cached = value.toByteArray(); + } + return cached; + } + @Override public byte[] getBytes() { - return getBytes(PrimitiveType.Bytes); + checkType(PrimitiveType.Bytes, type); + return value.toByteArray(); } @Override public byte[] getBytesUnsafe() { - return getBytesUnsafe(PrimitiveType.Bytes); + checkType(PrimitiveType.Bytes, type); + return getCachedBytes(); } @Override public ByteString getBytesAsByteString() { - return getByteString(PrimitiveType.Bytes); + checkType(PrimitiveType.Bytes, type); + return value; } @Override public byte[] getYson() { - return getBytes(PrimitiveType.Yson); + checkType(PrimitiveType.Yson, type); + return value.toByteArray(); } @Override public byte[] getYsonUnsafe() { - return getBytesUnsafe(PrimitiveType.Yson); + checkType(PrimitiveType.Yson, type); + return getCachedBytes(); } @Override public ByteString getYsonBytes() { - return getByteString(PrimitiveType.Yson); + checkType(PrimitiveType.Yson, type); + return value; } @Override @@ -1126,109 +1145,40 @@ public boolean equals(Object o) { if (type != that.type) { return false; } - - if (value instanceof byte[]) { - if (that.value instanceof byte[]) { - return Arrays.equals((byte[]) value, (byte[]) that.value); - } - return that.value.equals(UnsafeByteOperations.unsafeWrap((byte[]) value)); - } - - if (that.value instanceof byte[]) { - return value.equals(UnsafeByteOperations.unsafeWrap((byte[]) that.value)); - } return value.equals(that.value); } @Override public int hashCode() { - int result = type.hashCode(); - - if (value instanceof byte[]) { - for (byte b : (byte[]) value) { - result = 31 * result + b; - } - } else { - ByteString v = (ByteString) this.value; - for (int i = 0; i < v.size(); i++) { - byte b = v.byteAt(i); - result = 31 * result + b; - } - } - - return result; + return 31 * type.hashCode() + value.hashCode(); } @Override public String toString() { - final int length = (value instanceof byte[]) - ? ((byte[]) value).length - : ((ByteString) value).size(); - - if (length == 0) { - return "\"\""; + if (value.isEmpty()) { + return type.name() + "[len=0]"; } - // bytes are escaped as \nnn (octal value) - StringBuilder sb = new StringBuilder(length * 4 + 2); - sb.append('\"'); + StringBuilder sb = new StringBuilder(); + sb.append(type.name()); + sb.append("[len="); + sb.append(value.size()); + sb.append(" content="); - if (value instanceof byte[]) { - for (byte b : (byte[]) value) { - encodeAsOctal(sb, b); - } + if (value.size() <= OUTPUT_LIMIT) { + Hex.toHex(value, sb); } else { - ByteString bytes = (ByteString) this.value; - for (int i = 0; i < bytes.size(); i++) { - encodeAsOctal(sb, bytes.byteAt(i)); - } + Hex.toHex(value.substring(0, OUTPUT_LIMIT - 3), sb); + sb.append("..."); } - sb.append('\"'); - return sb.toString(); - } - private static void encodeAsOctal(StringBuilder sb, byte b) { - final int i = Byte.toUnsignedInt(b); - sb.append('\\'); - if (i < 64) { - sb.append('0'); - if (i < 8) { - sb.append('0'); - } - } - sb.append(Integer.toString(i, 8)); + sb.append("]"); + return sb.toString(); } @Override public ValueProtos.Value toPb() { - return ProtoValue.fromBytes(getByteString(type)); - } - - private byte[] getBytes(PrimitiveType expected) { - checkType(expected, type); - - if (value instanceof byte[]) { - return ((byte[]) value).clone(); - } - return ((ByteString) value).toByteArray(); - } - - private byte[] getBytesUnsafe(PrimitiveType expected) { - checkType(expected, type); - - if (value instanceof byte[]) { - return (byte[]) value; - } - return ((ByteString) value).toByteArray(); - } - - private ByteString getByteString(PrimitiveType expected) { - checkType(expected, type); - - if (value instanceof byte[]) { - return UnsafeByteOperations.unsafeWrap((byte[]) value); - } - return (ByteString) value; + return ProtoValue.fromBytes(value); } } diff --git a/table/src/test/java/tech/ydb/table/values/PrimitiveValueTest.java b/table/src/test/java/tech/ydb/table/values/PrimitiveValueTest.java index 8d47a0a2b..cdac62b86 100644 --- a/table/src/test/java/tech/ydb/table/values/PrimitiveValueTest.java +++ b/table/src/test/java/tech/ydb/table/values/PrimitiveValueTest.java @@ -327,15 +327,27 @@ public void double_inf() { @Test public void bytes() { byte[] data = { 0x0, 0x7, 0x3f, 0x7f, (byte) 0xff }; + byte[] other = { 0x0, 0x7, 0x34, 0x7f, (byte) 0xff }; Consumer doTest = (v) -> { + Assert.assertEquals(v, v); + + Assert.assertNotEquals(v, null); + Assert.assertNotEquals(v, PrimitiveValue.newYson(data)); + Assert.assertNotEquals(v, PrimitiveValue.newInt32(1)); + Assert.assertEquals(PrimitiveValue.newBytes(data), v); Assert.assertEquals(PrimitiveValue.newBytes(ByteString.copyFrom(data)), v); Assert.assertNotEquals(PrimitiveValue.newBytes(ByteString.EMPTY), v); + Assert.assertNotEquals(PrimitiveValue.newBytes(other), v); - Assert.assertEquals("\"\\000\\007\\077\\177\\377\"", v.toString()); + Assert.assertEquals("Bytes[len=5 content=00073f7fff]", v.toString()); Assert.assertArrayEquals(data, v.getBytes()); Assert.assertArrayEquals(data, v.getBytesUnsafe()); + + Assert.assertNotSame(v.getBytes(), v.getBytes()); + Assert.assertSame(v.getBytesUnsafe(), v.getBytesUnsafe()); + Assert.assertEquals(ByteString.copyFrom(data), v.getBytesAsByteString()); ValueProtos.Value vPb = v.toPb(); @@ -350,20 +362,48 @@ public void bytes() { // hashes must be the same Assert.assertEquals(PrimitiveValue.newBytes(data).hashCode(), PrimitiveValue.newBytes(ByteString.copyFrom(data)).hashCode()); + + Assert.assertEquals("Bytes[len=0]", PrimitiveValue.newBytes(ByteString.EMPTY).toString()); + + // toString must cut too long value + PrimitiveValue bytes51 = PrimitiveValue.newBytes(ByteString.fromHex("" + + "000102030405060708090A0B0C0D0E0F" + + "101112131415161718191A1B1C1D1E1F" + + "202122232425262728292A2B2C2D2E2F" + + "303132" + )); + Assert.assertEquals("Bytes[len=51 content=" + + "000102030405060708090a0b0c0d0e0f" + + "101112131415161718191a1b1c1d1e1f" + + "202122232425262728292a2b2c2d2e..." + + "]", bytes51.toString()); } @Test public void yson() { byte[] data = { 0x0, 0x7, 0x3f, 0x7f, (byte) 0xff }; + byte[] other = { 0x0, 0x7, 0x34, 0x7f, (byte) 0xff }; Consumer doTest = (v) -> { + Assert.assertEquals(v, v); + + Assert.assertNotEquals(v, null); + Assert.assertNotEquals(v, PrimitiveValue.newBytes(data)); + Assert.assertNotEquals(v, PrimitiveValue.newInt32(1)); + Assert.assertEquals(PrimitiveValue.newYson(data), v); Assert.assertEquals(PrimitiveValue.newYson(ByteString.copyFrom(data)), v); Assert.assertNotEquals(PrimitiveValue.newYson(ByteString.EMPTY), v); + Assert.assertNotEquals(PrimitiveValue.newYson(other), v); + + Assert.assertEquals("Yson[len=5 content=00073f7fff]", v.toString()); - Assert.assertEquals("\"\\000\\007\\077\\177\\377\"", v.toString()); Assert.assertArrayEquals(data, v.getYson()); Assert.assertArrayEquals(data, v.getYsonUnsafe()); + + Assert.assertNotSame(v.getYson(), v.getYson()); + Assert.assertSame(v.getYsonUnsafe(), v.getYsonUnsafe()); + Assert.assertEquals(ByteString.copyFrom(data), v.getYsonBytes()); ValueProtos.Value vPb = v.toPb(); @@ -378,6 +418,21 @@ public void yson() { // hashes must be the same Assert.assertEquals(PrimitiveValue.newYson(data).hashCode(), PrimitiveValue.newYson(ByteString.copyFrom(data)).hashCode()); + + Assert.assertEquals("Yson[len=0]", PrimitiveValue.newYson(ByteString.EMPTY).toString()); + + // toString must cut too long value + PrimitiveValue yson51 = PrimitiveValue.newYson(ByteString.fromHex("" + + "000102030405060708090A0B0C0D0E0F" + + "101112131415161718191A1B1C1D1E1F" + + "202122232425262728292A2B2C2D2E2F" + + "303132" + )); + Assert.assertEquals("Yson[len=51 content=" + + "000102030405060708090a0b0c0d0e0f" + + "101112131415161718191a1b1c1d1e1f" + + "202122232425262728292a2b2c2d2e..." + + "]", yson51.toString()); } @Test From 49cf8af8b7ae8cfbec74e67f29e4441ef801e2ce Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Mon, 9 Feb 2026 12:29:40 +0000 Subject: [PATCH 2/2] Small updates --- .../tech/ydb/table/values/PrimitiveValue.java | 48 ++++++++++++------- .../result/impl/ProtoValueReaderTest.java | 2 +- .../ydb/table/values/PrimitiveValueTest.java | 21 ++++++-- 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/table/src/main/java/tech/ydb/table/values/PrimitiveValue.java b/table/src/main/java/tech/ydb/table/values/PrimitiveValue.java index 3e900556d..1faa99689 100644 --- a/table/src/main/java/tech/ydb/table/values/PrimitiveValue.java +++ b/table/src/main/java/tech/ydb/table/values/PrimitiveValue.java @@ -229,15 +229,25 @@ public static PrimitiveValue newDouble(double value) { } public static PrimitiveValue newBytes(byte[] value) { - return value.length == 0 ? Bytes.EMPTY_STRING : new Bytes(PrimitiveType.Bytes, value.clone()); + if (value.length == 0) { + return Bytes.EMPTY_BYTES; + } + return new Bytes(PrimitiveType.Bytes, ByteString.copyFrom(value), value); } public static PrimitiveValue newBytes(ByteString value) { - return value.isEmpty() ? Bytes.EMPTY_STRING : new Bytes(PrimitiveType.Bytes, value); + if (value.isEmpty()) { + return Bytes.EMPTY_BYTES; + } + + return new Bytes(PrimitiveType.Bytes, value); } public static PrimitiveValue newBytesOwn(byte[] value) { - return value.length == 0 ? Bytes.EMPTY_STRING : new Bytes(PrimitiveType.Bytes, value); + if (value.length == 0) { + return Bytes.EMPTY_BYTES; + } + return new Bytes(PrimitiveType.Bytes, UnsafeByteOperations.unsafeWrap(value), value); } public static PrimitiveValue newText(String value) { @@ -245,15 +255,24 @@ public static PrimitiveValue newText(String value) { } public static PrimitiveValue newYson(byte[] value) { - return value.length == 0 ? Bytes.EMPTY_YSON : new Bytes(PrimitiveType.Yson, value.clone()); + if (value.length == 0) { + return Bytes.EMPTY_YSON; + } + return new Bytes(PrimitiveType.Yson, ByteString.copyFrom(value), value); } public static PrimitiveValue newYson(ByteString value) { - return value.isEmpty() ? Bytes.EMPTY_YSON : new Bytes(PrimitiveType.Yson, value); + if (value.isEmpty()) { + return Bytes.EMPTY_YSON; + } + return new Bytes(PrimitiveType.Yson, value); } public static PrimitiveValue newYsonOwn(byte[] value) { - return value.length == 0 ? Bytes.EMPTY_YSON : new Bytes(PrimitiveType.Yson, value); + if (value.length == 0) { + return Bytes.EMPTY_YSON; + } + return new Bytes(PrimitiveType.Yson, UnsafeByteOperations.unsafeWrap(value), value); } public static PrimitiveValue newJson(String value) { @@ -1061,26 +1080,23 @@ public ValueProtos.Value toPb() { private static final class Bytes extends PrimitiveValue { private static final int OUTPUT_LIMIT = 50; - private static final Bytes EMPTY_STRING = new Bytes(PrimitiveType.Bytes, new byte[0]); - private static final Bytes EMPTY_YSON = new Bytes(PrimitiveType.Yson, new byte[0]); + private static final Bytes EMPTY_BYTES = new Bytes(PrimitiveType.Bytes, ByteString.EMPTY); + private static final Bytes EMPTY_YSON = new Bytes(PrimitiveType.Yson, ByteString.EMPTY); private static final long serialVersionUID = 1523630543323446576L; private final PrimitiveType type; private final ByteString value; - @SuppressWarnings("VolatileArrayField") - private volatile byte[] cached; + private transient byte[] cached; - private Bytes(PrimitiveType type, byte[] value) { - this.type = type; - this.value = UnsafeByteOperations.unsafeWrap(value); - this.cached = value; + private Bytes(PrimitiveType type, ByteString value) { + this(type, value, null); } - private Bytes(PrimitiveType type, ByteString value) { + private Bytes(PrimitiveType type, ByteString value, byte[] cached) { this.type = type; this.value = value; - this.cached = null; + this.cached = cached; } @Override diff --git a/table/src/test/java/tech/ydb/table/result/impl/ProtoValueReaderTest.java b/table/src/test/java/tech/ydb/table/result/impl/ProtoValueReaderTest.java index d20d610ce..e6b0660d6 100644 --- a/table/src/test/java/tech/ydb/table/result/impl/ProtoValueReaderTest.java +++ b/table/src/test/java/tech/ydb/table/result/impl/ProtoValueReaderTest.java @@ -523,7 +523,7 @@ public void textTypesTest() { @Test public void bytesTypesTest() { - byte[] arr = "[1,2,3]".getBytes(); + byte[] arr = new byte[] { '[', '1', ',', '2', ',', '3', ']' }; ValueProtos.Value bytes = ValueProtos.Value.newBuilder().setBytesValue(ByteString.copyFrom(arr)).build(); { diff --git a/table/src/test/java/tech/ydb/table/values/PrimitiveValueTest.java b/table/src/test/java/tech/ydb/table/values/PrimitiveValueTest.java index cdac62b86..19bcb47ad 100644 --- a/table/src/test/java/tech/ydb/table/values/PrimitiveValueTest.java +++ b/table/src/test/java/tech/ydb/table/values/PrimitiveValueTest.java @@ -344,12 +344,12 @@ public void bytes() { Assert.assertEquals("Bytes[len=5 content=00073f7fff]", v.toString()); Assert.assertArrayEquals(data, v.getBytes()); Assert.assertArrayEquals(data, v.getBytesUnsafe()); + Assert.assertEquals(ByteString.copyFrom(data), v.getBytesAsByteString()); + Assert.assertNotSame(v.getBytes(), data); // always copy Assert.assertNotSame(v.getBytes(), v.getBytes()); Assert.assertSame(v.getBytesUnsafe(), v.getBytesUnsafe()); - Assert.assertEquals(ByteString.copyFrom(data), v.getBytesAsByteString()); - ValueProtos.Value vPb = v.toPb(); Assert.assertEquals(vPb, ProtoValue.fromBytes(data)); Assert.assertTrue(ProtoValue.fromPb(PrimitiveType.Bytes, vPb).equals(v)); @@ -359,11 +359,17 @@ public void bytes() { doTest.accept(PrimitiveValue.newBytesOwn(data)); doTest.accept(PrimitiveValue.newBytes(ByteString.copyFrom(data))); + Assert.assertSame(PrimitiveValue.newBytes(data).getBytesUnsafe(), data); + Assert.assertSame(PrimitiveValue.newBytesOwn(data).getBytesUnsafe(), data); + Assert.assertNotSame(PrimitiveValue.newBytes(ByteString.copyFrom(data)).getBytesUnsafe(), data); + // hashes must be the same Assert.assertEquals(PrimitiveValue.newBytes(data).hashCode(), PrimitiveValue.newBytes(ByteString.copyFrom(data)).hashCode()); Assert.assertEquals("Bytes[len=0]", PrimitiveValue.newBytes(ByteString.EMPTY).toString()); + Assert.assertEquals("Bytes[len=0]", PrimitiveValue.newBytes(new byte[0]).toString()); + Assert.assertEquals("Bytes[len=0]", PrimitiveValue.newBytesOwn(new byte[0]).toString()); // toString must cut too long value PrimitiveValue bytes51 = PrimitiveValue.newBytes(ByteString.fromHex("" @@ -397,15 +403,14 @@ public void yson() { Assert.assertNotEquals(PrimitiveValue.newYson(other), v); Assert.assertEquals("Yson[len=5 content=00073f7fff]", v.toString()); - Assert.assertArrayEquals(data, v.getYson()); Assert.assertArrayEquals(data, v.getYsonUnsafe()); + Assert.assertEquals(ByteString.copyFrom(data), v.getYsonBytes()); + Assert.assertNotSame(v.getYson(), data); // always copy Assert.assertNotSame(v.getYson(), v.getYson()); Assert.assertSame(v.getYsonUnsafe(), v.getYsonUnsafe()); - Assert.assertEquals(ByteString.copyFrom(data), v.getYsonBytes()); - ValueProtos.Value vPb = v.toPb(); Assert.assertEquals(vPb, ProtoValue.fromYson(data)); Assert.assertTrue(ProtoValue.fromPb(PrimitiveType.Yson, vPb).equals(v)); @@ -415,11 +420,17 @@ public void yson() { doTest.accept(PrimitiveValue.newYsonOwn(data)); doTest.accept(PrimitiveValue.newYson(ByteString.copyFrom(data))); + Assert.assertSame(PrimitiveValue.newYson(data).getYsonUnsafe(), data); + Assert.assertSame(PrimitiveValue.newYsonOwn(data).getYsonUnsafe(), data); + Assert.assertNotSame(PrimitiveValue.newYson(ByteString.copyFrom(data)).getYsonUnsafe(), data); + // hashes must be the same Assert.assertEquals(PrimitiveValue.newYson(data).hashCode(), PrimitiveValue.newYson(ByteString.copyFrom(data)).hashCode()); Assert.assertEquals("Yson[len=0]", PrimitiveValue.newYson(ByteString.EMPTY).toString()); + Assert.assertEquals("Yson[len=0]", PrimitiveValue.newYson(new byte[0]).toString()); + Assert.assertEquals("Yson[len=0]", PrimitiveValue.newYsonOwn(new byte[0]).toString()); // toString must cut too long value PrimitiveValue yson51 = PrimitiveValue.newYson(ByteString.fromHex(""