diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SsFormat.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SsFormat.java
new file mode 100644
index 0000000000..1882a12a83
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SsFormat.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.spanner.spi.v1;
+
+import com.google.protobuf.ByteString;
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+
+public final class SsFormat {
+
+ /**
+ * Makes the given key a prefix successor. This means that the returned key is the smallest
+ * possible key that is larger than the input key, and that does not have the input key as a
+ * prefix.
+ *
+ *
This is done by flipping the least significant bit of the last byte of the key.
+ *
+ * @param key The key to make a prefix successor.
+ * @return The prefix successor key.
+ */
+ public static ByteString makePrefixSuccessor(ByteString key) {
+ if (key == null || key.isEmpty()) {
+ return ByteString.EMPTY;
+ }
+ byte[] bytes = key.toByteArray();
+ if (bytes.length > 0) {
+ bytes[bytes.length - 1] = (byte) (bytes[bytes.length - 1] | 1);
+ }
+ return ByteString.copyFrom(bytes);
+ }
+
+ private SsFormat() {}
+
+ private static final int IS_KEY = 0x80;
+ private static final int TYPE_MASK = 0x7f;
+
+ // HeaderType enum values (selected)
+ private static final int TYPE_UINT_1 = 0;
+ private static final int TYPE_UINT_9 = 8;
+ private static final int TYPE_NEG_INT_8 = 9;
+ private static final int TYPE_NEG_INT_1 = 16;
+ private static final int TYPE_POS_INT_1 = 17;
+ private static final int TYPE_POS_INT_8 = 24;
+ private static final int TYPE_STRING = 25;
+ private static final int TYPE_NULL_ORDERED_FIRST = 27;
+ private static final int TYPE_NULLABLE_NOT_NULL_NULL_ORDERED_FIRST = 28;
+ private static final int TYPE_DECREASING_UINT_9 = 32;
+ private static final int TYPE_DECREASING_UINT_1 = 40;
+ private static final int TYPE_DECREASING_NEG_INT_8 = 41;
+ private static final int TYPE_DECREASING_NEG_INT_1 = 48;
+ private static final int TYPE_DECREASING_POS_INT_1 = 49;
+ private static final int TYPE_DECREASING_POS_INT_8 = 56;
+ private static final int TYPE_DECREASING_STRING = 57;
+ private static final int TYPE_NULLABLE_NOT_NULL_NULL_ORDERED_LAST = 59;
+ private static final int TYPE_NULL_ORDERED_LAST = 60;
+ private static final int TYPE_NEG_DOUBLE_8 = 66;
+ private static final int TYPE_NEG_DOUBLE_1 = 73;
+ private static final int TYPE_POS_DOUBLE_1 = 74;
+ private static final int TYPE_POS_DOUBLE_8 = 81;
+ private static final int TYPE_DECREASING_NEG_DOUBLE_8 = 82;
+ private static final int TYPE_DECREASING_NEG_DOUBLE_1 = 89;
+ private static final int TYPE_DECREASING_POS_DOUBLE_1 = 90;
+ private static final int TYPE_DECREASING_POS_DOUBLE_8 = 97;
+
+ // EscapeChar enum values
+ private static final byte ASCENDING_ZERO_ESCAPE = (byte) 0xf0;
+ private static final byte ASCENDING_FF_ESCAPE = (byte) 0x10;
+ private static final byte SEP = (byte) 0x78; // 'x'
+
+ // For AppendCompositeTag
+ private static final int K_OBJECT_EXISTENCE_TAG = 0x7e;
+ private static final int K_MAX_FIELD_TAG = 0xffff;
+
+ public static void appendCompositeTag(ByteArrayOutputStream out, int tag) {
+ if (tag == K_OBJECT_EXISTENCE_TAG || tag <= 0 || tag > K_MAX_FIELD_TAG) {
+ throw new IllegalArgumentException("Invalid tag value: " + tag);
+ }
+
+ if (tag < 16) {
+ // Short tag: 000 TTTT S (S is LSB of tag, but here tag is original, so S=0)
+ // Encodes as (tag << 1)
+ out.write((byte) (tag << 1));
+ } else {
+ // Long tag
+ int shiftedTag = tag << 1; // LSB is 0 for prefix successor
+ if (shiftedTag < (1 << (5 + 8))) { // Original tag < 4096
+ // Header: num_extra_bytes=1 (01xxxxx), P=payload bits from tag
+ // (1 << 5) is 00100000
+ // (shiftedTag >> 8) are the 5 MSBs of the payload part of the tag
+ out.write((byte) ((1 << 5) | (shiftedTag >> 8)));
+ out.write((byte) (shiftedTag & 0xFF));
+ } else { // Original tag >= 4096 and <= K_MAX_FIELD_TAG (65535)
+ // Header: num_extra_bytes=2 (10xxxxx)
+ // (2 << 5) is 01000000
+ out.write((byte) ((2 << 5) | (shiftedTag >> 16)));
+ out.write((byte) ((shiftedTag >> 8) & 0xFF));
+ out.write((byte) (shiftedTag & 0xFF));
+ }
+ }
+ }
+
+ public static void appendNullOrderedFirst(ByteArrayOutputStream out) {
+ out.write((byte) (IS_KEY | TYPE_NULL_ORDERED_FIRST));
+ out.write((byte) 0);
+ }
+
+ public static void appendNullOrderedLast(ByteArrayOutputStream out) {
+ out.write((byte) (IS_KEY | TYPE_NULL_ORDERED_LAST));
+ out.write((byte) 0);
+ }
+
+ public static void appendNotNullMarkerNullOrderedFirst(ByteArrayOutputStream out) {
+ out.write((byte) (IS_KEY | TYPE_NULLABLE_NOT_NULL_NULL_ORDERED_FIRST));
+ }
+
+ public static void appendNotNullMarkerNullOrderedLast(ByteArrayOutputStream out) {
+ out.write((byte) (IS_KEY | TYPE_NULLABLE_NOT_NULL_NULL_ORDERED_LAST));
+ }
+
+ public static void appendUnsignedIntIncreasing(ByteArrayOutputStream out, long val) {
+ if (val < 0) {
+ throw new IllegalArgumentException("Unsigned int cannot be negative: " + val);
+ }
+ byte[] buf = new byte[9]; // Max 9 bytes for value payload
+ int len = 0;
+
+ long tempVal = val;
+ buf[8 - len] = (byte) ((tempVal & 0x7F) << 1); // LSB is prefix-successor bit (0)
+ tempVal >>= 7;
+ len++;
+
+ while (tempVal > 0) {
+ buf[8 - len] = (byte) (tempVal & 0xFF);
+ tempVal >>= 8;
+ len++;
+ }
+
+ out.write((byte) (IS_KEY | (TYPE_UINT_1 + len - 1)));
+ for (int i = 0; i < len; i++) {
+ out.write((byte) (buf[8 - len + 1 + i] & 0xFF));
+ }
+ }
+
+ public static void appendUnsignedIntDecreasing(ByteArrayOutputStream out, long val) {
+ if (val < 0) {
+ throw new IllegalArgumentException("Unsigned int cannot be negative: " + val);
+ }
+ byte[] buf = new byte[9];
+ int len = 0;
+ long tempVal = val;
+
+ // InvertByte(val & 0x7f) << 1
+ buf[8 - len] = (byte) ((~(tempVal & 0x7F) & 0x7F) << 1);
+ tempVal >>= 7;
+ len++;
+
+ while (tempVal > 0) {
+ buf[8 - len] = (byte) (~(tempVal & 0xFF));
+ tempVal >>= 8;
+ len++;
+ }
+ // If val was 0, loop doesn't run for len > 1. If len is still 1, all bits of tempVal (0) are
+ // covered.
+ // If val was large, but remaining tempVal became 0, this is correct.
+ // If tempVal was 0 initially, buf[8] has (~0 & 0x7f) << 1. len = 1.
+ // If tempVal was >0 but became 0 after some shifts, buf[8-len] has inverted last byte.
+
+ out.write((byte) (IS_KEY | (TYPE_DECREASING_UINT_1 - len + 1)));
+ for (int i = 0; i < len; i++) {
+ out.write((byte) (buf[8 - len + 1 + i] & 0xFF));
+ }
+ }
+
+ private static void appendIntInternal(
+ ByteArrayOutputStream out, long val, boolean decreasing, boolean isDouble) {
+ if (decreasing) {
+ val = ~val;
+ }
+
+ byte[] buf = new byte[8]; // Max 8 bytes for payload
+ int len = 0;
+ long tempVal = val;
+
+ if (tempVal >= 0) {
+ buf[7 - len] = (byte) ((tempVal & 0x7F) << 1);
+ tempVal >>= 7;
+ len++;
+ while (tempVal > 0) {
+ buf[7 - len] = (byte) (tempVal & 0xFF);
+ tempVal >>= 8;
+ len++;
+ }
+ } else { // tempVal < 0
+ // For negative numbers, extend sign bit after shifting
+ buf[7 - len] = (byte) ((tempVal & 0x7F) << 1);
+ // Simulate sign extension for right shift of negative number
+ // (x >> 7) | 0xFE00000000000000ULL; (if x has 64 bits)
+ // In Java, right shift `>>` on negative longs performs sign extension.
+ tempVal >>= 7;
+ len++;
+ while (tempVal != -1L) { // Loop until all remaining bits are 1s (sign extension)
+ buf[7 - len] = (byte) (tempVal & 0xFF);
+ tempVal >>= 8;
+ len++;
+ if (len > 8) throw new AssertionError("Signed int encoding overflow");
+ }
+ }
+
+ int type;
+ if (val >= 0) { // Original val before potential bit-negation for decreasing
+ if (!decreasing) {
+ type = isDouble ? (TYPE_POS_DOUBLE_1 + len - 1) : (TYPE_POS_INT_1 + len - 1);
+ } else {
+ type =
+ isDouble
+ ? (TYPE_DECREASING_POS_DOUBLE_1 + len - 1)
+ : (TYPE_DECREASING_POS_INT_1 + len - 1);
+ }
+ } else {
+ if (!decreasing) {
+ type = isDouble ? (TYPE_NEG_DOUBLE_1 - len + 1) : (TYPE_NEG_INT_1 - len + 1);
+ } else {
+ type =
+ isDouble
+ ? (TYPE_DECREASING_NEG_DOUBLE_1 - len + 1)
+ : (TYPE_DECREASING_NEG_INT_1 - len + 1);
+ }
+ }
+ out.write((byte) (IS_KEY | type));
+ for (int i = 0; i < len; i++) {
+ out.write((byte) (buf[7 - len + 1 + i] & 0xFF));
+ }
+ }
+
+ public static void appendIntIncreasing(ByteArrayOutputStream out, long value) {
+ appendIntInternal(out, value, false, false);
+ }
+
+ public static void appendIntDecreasing(ByteArrayOutputStream out, long value) {
+ appendIntInternal(out, value, true, false);
+ }
+
+ public static void appendDoubleIncreasing(ByteArrayOutputStream out, double value) {
+ long enc = Double.doubleToRawLongBits(value);
+ if (enc < 0) {
+ // Transform negative doubles to maintain lexicographic sort order
+ enc = Long.MIN_VALUE - enc;
+ }
+ appendIntInternal(out, enc, false, true);
+ }
+
+ public static void appendDoubleDecreasing(ByteArrayOutputStream out, double value) {
+ long enc = Double.doubleToRawLongBits(value);
+ if (enc < 0) {
+ enc = Long.MIN_VALUE - enc;
+ }
+ appendIntInternal(out, enc, true, true);
+ }
+
+ private static void appendByteSequence(
+ ByteArrayOutputStream out, byte[] bytes, boolean decreasing) {
+ out.write((byte) (IS_KEY | (decreasing ? TYPE_DECREASING_STRING : TYPE_STRING)));
+
+ for (byte b : bytes) {
+ byte currentByte = decreasing ? (byte) ~b : b;
+ int unsignedByte = currentByte & 0xFF;
+ if (unsignedByte == 0x00) {
+ out.write((byte) 0x00);
+ out.write(
+ decreasing
+ ? ASCENDING_ZERO_ESCAPE
+ : ASCENDING_ZERO_ESCAPE); // After inversion, 0xFF becomes 0x00. Escape for 0x00
+ // (inverted) is F0.
+ // If increasing, 0x00 -> 0x00 F0.
+ } else if (unsignedByte == 0xFF) {
+ out.write((byte) 0xFF);
+ out.write(
+ decreasing
+ ? ASCENDING_FF_ESCAPE
+ : ASCENDING_FF_ESCAPE); // After inversion, 0x00 becomes 0xFF. Escape for 0xFF
+ // (inverted) is 0x10.
+ // If increasing, 0xFF -> 0xFF 0x10.
+ } else {
+ out.write((byte) unsignedByte);
+ }
+ }
+ // Terminator
+ out.write((byte) (decreasing ? 0xFF : 0x00));
+ out.write(SEP);
+ }
+
+ public static void appendStringIncreasing(ByteArrayOutputStream out, String value) {
+ appendByteSequence(out, value.getBytes(StandardCharsets.UTF_8), false);
+ }
+
+ public static void appendStringDecreasing(ByteArrayOutputStream out, String value) {
+ appendByteSequence(out, value.getBytes(StandardCharsets.UTF_8), true);
+ }
+
+ public static void appendBytesIncreasing(ByteArrayOutputStream out, byte[] value) {
+ appendByteSequence(out, value, false);
+ }
+
+ public static void appendBytesDecreasing(ByteArrayOutputStream out, byte[] value) {
+ appendByteSequence(out, value, true);
+ }
+
+ /**
+ * Encodes a timestamp as 12 bytes: 8 bytes for seconds since epoch (with offset to handle
+ * negative), 4 bytes for nanoseconds.
+ */
+ public static byte[] encodeTimestamp(long seconds, int nanos) {
+ // Add offset to make negative seconds sort correctly
+ long kSecondsOffset = 1L << 63;
+ long hi = seconds + kSecondsOffset;
+ int lo = nanos;
+
+ byte[] buf = new byte[12];
+ // Big-endian encoding
+ buf[0] = (byte) (hi >> 56);
+ buf[1] = (byte) (hi >> 48);
+ buf[2] = (byte) (hi >> 40);
+ buf[3] = (byte) (hi >> 32);
+ buf[4] = (byte) (hi >> 24);
+ buf[5] = (byte) (hi >> 16);
+ buf[6] = (byte) (hi >> 8);
+ buf[7] = (byte) hi;
+ buf[8] = (byte) (lo >> 24);
+ buf[9] = (byte) (lo >> 16);
+ buf[10] = (byte) (lo >> 8);
+ buf[11] = (byte) lo;
+ return buf;
+ }
+
+ /** Encodes a UUID (128-bit) as 16 bytes in big-endian order. */
+ public static byte[] encodeUuid(long high, long low) {
+ byte[] buf = new byte[16];
+ // Big-endian encoding
+ buf[0] = (byte) (high >> 56);
+ buf[1] = (byte) (high >> 48);
+ buf[2] = (byte) (high >> 40);
+ buf[3] = (byte) (high >> 32);
+ buf[4] = (byte) (high >> 24);
+ buf[5] = (byte) (high >> 16);
+ buf[6] = (byte) (high >> 8);
+ buf[7] = (byte) high;
+ buf[8] = (byte) (low >> 56);
+ buf[9] = (byte) (low >> 48);
+ buf[10] = (byte) (low >> 40);
+ buf[11] = (byte) (low >> 32);
+ buf[12] = (byte) (low >> 24);
+ buf[13] = (byte) (low >> 16);
+ buf[14] = (byte) (low >> 8);
+ buf[15] = (byte) low;
+ return buf;
+ }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/TargetRange.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/TargetRange.java
new file mode 100644
index 0000000000..bfcd2e30a8
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/TargetRange.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.spanner.spi.v1;
+
+import com.google.api.core.InternalApi;
+import com.google.protobuf.ByteString;
+
+/** Represents a key range with start and limit boundaries for routing. */
+@InternalApi
+public class TargetRange {
+ public ByteString start;
+ public ByteString limit;
+ public boolean approximate;
+
+ public TargetRange(ByteString start, ByteString limit, boolean approximate) {
+ this.start = start;
+ this.limit = limit;
+ this.approximate = approximate;
+ }
+
+ public boolean isPoint() {
+ return limit.isEmpty();
+ }
+
+ /**
+ * Merges another TargetRange into this one. The resulting range will be the union of the two
+ * ranges, taking the minimum start key and maximum limit key.
+ */
+ public void mergeFrom(TargetRange other) {
+ if (ByteString.unsignedLexicographicalComparator().compare(other.start, this.start) < 0) {
+ this.start = other.start;
+ }
+ if (other.isPoint()
+ && ByteString.unsignedLexicographicalComparator().compare(other.start, this.limit) >= 0) {
+ this.limit = SsFormat.makePrefixSuccessor(other.start);
+ } else if (ByteString.unsignedLexicographicalComparator().compare(other.limit, this.limit)
+ > 0) {
+ this.limit = other.limit;
+ }
+ this.approximate |= other.approximate;
+ }
+}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/SsFormatTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/SsFormatTest.java
new file mode 100644
index 0000000000..2641f28f1b
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/SsFormatTest.java
@@ -0,0 +1,834 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.spanner.spi.v1;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import com.google.protobuf.ByteString;
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.TreeSet;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link SsFormat}. */
+@RunWith(JUnit4.class)
+public class SsFormatTest {
+
+ private static List signedIntTestValues;
+ private static List unsignedIntTestValues;
+ private static List doubleTestValues;
+
+ /** Comparator for unsigned lexicographic comparison of byte arrays. */
+ private static final Comparator UNSIGNED_BYTE_COMPARATOR =
+ (a, b) ->
+ ByteString.unsignedLexicographicalComparator()
+ .compare(ByteString.copyFrom(a), ByteString.copyFrom(b));
+
+ @BeforeClass
+ public static void setUpTestData() {
+ signedIntTestValues = buildSignedIntTestValues();
+ unsignedIntTestValues = buildUnsignedIntTestValues();
+ doubleTestValues = buildDoubleTestValues();
+ }
+
+ private static List buildSignedIntTestValues() {
+ TreeSet values = new TreeSet<>();
+
+ // Range of small values
+ for (int i = -300; i < 300; i++) {
+ values.add((long) i);
+ }
+
+ // Powers of 2 and boundaries
+ for (int i = 0; i < 63; i++) {
+ long powerOf2 = 1L << i;
+ values.add(powerOf2);
+ values.add(powerOf2 - 1);
+ values.add(powerOf2 + 1);
+ values.add(-powerOf2);
+ values.add(-powerOf2 - 1);
+ values.add(-powerOf2 + 1);
+ }
+
+ // Edge cases
+ values.add(Long.MIN_VALUE);
+ values.add(Long.MAX_VALUE);
+
+ return new ArrayList<>(values);
+ }
+
+ private static List buildUnsignedIntTestValues() {
+ TreeSet values = new TreeSet<>(Long::compareUnsigned);
+
+ // Range of small values
+ for (int i = 0; i < 600; i++) {
+ values.add((long) i);
+ }
+
+ // Powers of 2 and boundaries (treating as unsigned)
+ for (int i = 0; i < 64; i++) {
+ long powerOf2 = 1L << i;
+ values.add(powerOf2);
+ if (powerOf2 > 0) {
+ values.add(powerOf2 - 1);
+ }
+ values.add(powerOf2 + 1);
+ }
+
+ // Max unsigned value (all bits set)
+ values.add(-1L); // 0xFFFFFFFFFFFFFFFF as unsigned
+
+ return new ArrayList<>(values);
+ }
+
+ private static List buildDoubleTestValues() {
+ TreeSet values =
+ new TreeSet<>(
+ (a, b) -> {
+ // Handle NaN specially - put at end
+ if (Double.isNaN(a) && Double.isNaN(b)) return 0;
+ if (Double.isNaN(a)) return 1;
+ if (Double.isNaN(b)) return -1;
+ return Double.compare(a, b);
+ });
+
+ // Basic values
+ values.add(0.0);
+ values.add(-0.0);
+ values.add(Double.POSITIVE_INFINITY);
+ values.add(Double.NEGATIVE_INFINITY);
+ values.add(Double.MIN_VALUE);
+ values.add(Double.MAX_VALUE);
+ values.add(-Double.MIN_VALUE);
+ values.add(-Double.MAX_VALUE);
+
+ // Powers of 10
+ double value = 1.0;
+ for (int i = 0; i < 10; i++) {
+ values.add(value);
+ values.add(-value);
+ value /= 10;
+ }
+
+ long[] signs = {0, 1};
+ long[] exponents = {
+ 0, 1, 2, 100, 200, 512, 1000, 1020, 1021, 1022, 1023, 1024, 1025, 1026, 1027, 1028, 1029,
+ 2000, 2045, 2046, 2047
+ };
+ long[] fractions = {
+ 0,
+ 1,
+ 2,
+ 10,
+ 16,
+ 255,
+ 256,
+ 32767,
+ 32768,
+ 65535,
+ 65536,
+ 1000000,
+ 0x7ffffffeL,
+ 0x7fffffffL,
+ 0x80000000L,
+ 0x80000001L,
+ 0x80000002L,
+ 0x0003456789abcdefL,
+ 0x0007fffffffffffeL,
+ 0x0007ffffffffffffL,
+ 0x0008000000000000L,
+ 0x0008000000000001L,
+ 0x000cba9876543210L,
+ 0x000fffffffff0000L,
+ 0x000ffffffffff000L,
+ 0x000fffffffffff00L,
+ 0x000ffffffffffff0L,
+ 0x000ffffffffffff8L,
+ 0x000ffffffffffffcL,
+ 0x000ffffffffffffeL,
+ 0x000fffffffffffffL
+ };
+
+ for (long sign : signs) {
+ for (long exponent : exponents) {
+ for (long fraction : fractions) {
+ long bits = (sign << 63) | (exponent << 52) | fraction;
+ values.add(Double.longBitsToDouble(bits));
+ }
+ }
+ }
+
+ return new ArrayList<>(values);
+ }
+
+ // ==================== Prefix Successor Tests ====================
+
+ @Test
+ public void makePrefixSuccessor_emptyInput_returnsEmpty() {
+ assertEquals(ByteString.EMPTY, SsFormat.makePrefixSuccessor(ByteString.EMPTY));
+ assertEquals(ByteString.EMPTY, SsFormat.makePrefixSuccessor(null));
+ }
+
+ @Test
+ public void makePrefixSuccessor_singleByte_setsLsb() {
+ ByteString input = ByteString.copyFrom(new byte[] {0x00});
+ ByteString result = SsFormat.makePrefixSuccessor(input);
+
+ assertEquals(1, result.size());
+ assertEquals(0x01, result.byteAt(0) & 0xFF);
+ }
+
+ @Test
+ public void makePrefixSuccessor_multipleBytes_onlyModifiesLastByte() {
+ ByteString input = ByteString.copyFrom(new byte[] {0x12, 0x34, 0x00});
+ ByteString result = SsFormat.makePrefixSuccessor(input);
+
+ assertEquals(3, result.size());
+ assertEquals(0x12, result.byteAt(0) & 0xFF);
+ assertEquals(0x34, result.byteAt(1) & 0xFF);
+ assertEquals(0x01, result.byteAt(2) & 0xFF);
+ }
+
+ @Test
+ public void makePrefixSuccessor_resultIsGreaterThanOriginal() {
+ byte[] original = new byte[] {0x10, 0x20, 0x30};
+ ByteString successor = SsFormat.makePrefixSuccessor(ByteString.copyFrom(original));
+
+ assertTrue(
+ ByteString.unsignedLexicographicalComparator()
+ .compare(ByteString.copyFrom(original), successor)
+ < 0);
+ }
+
+ // ==================== Composite Tag Tests ====================
+
+ @Test
+ public void appendCompositeTag_shortTag_encodesInOneByte() {
+ // Tags 1-15 should fit in 1 byte
+ for (int tag = 1; tag <= 15; tag++) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ SsFormat.appendCompositeTag(out, tag);
+ byte[] result = out.toByteArray();
+
+ assertEquals("Tag " + tag + " should encode to 1 byte", 1, result.length);
+ assertEquals("Tag " + tag + " should encode as tag << 1", tag << 1, result[0] & 0xFF);
+ }
+ }
+
+ @Test
+ public void appendCompositeTag_mediumTag_encodesInTwoBytes() {
+ // Tags 16-4095 should fit in 2 bytes
+ int[] testTags = {16, 100, 1000, 4095};
+ for (int tag : testTags) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ SsFormat.appendCompositeTag(out, tag);
+ byte[] result = out.toByteArray();
+
+ assertEquals("Tag " + tag + " should encode to 2 bytes", 2, result.length);
+ }
+ }
+
+ @Test
+ public void appendCompositeTag_largeTag_encodesInThreeBytes() {
+ // Tags 4096-65535 should fit in 3 bytes
+ int[] testTags = {4096, 10000, 65535};
+ for (int tag : testTags) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ SsFormat.appendCompositeTag(out, tag);
+ byte[] result = out.toByteArray();
+
+ assertEquals("Tag " + tag + " should encode to 3 bytes", 3, result.length);
+ }
+ }
+
+ @Test
+ public void appendCompositeTag_invalidTag_throws() {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ assertThrows(IllegalArgumentException.class, () -> SsFormat.appendCompositeTag(out, 0));
+ assertThrows(IllegalArgumentException.class, () -> SsFormat.appendCompositeTag(out, -1));
+ assertThrows(IllegalArgumentException.class, () -> SsFormat.appendCompositeTag(out, 65536));
+ }
+
+ @Test
+ public void appendCompositeTag_preservesOrdering() {
+ // Verify smaller tags encode to lexicographically smaller byte sequences
+ for (int tag1 = 1; tag1 <= 100; tag1++) {
+ for (int tag2 = tag1 + 1; tag2 <= 101 && tag2 <= tag1 + 10; tag2++) {
+ ByteArrayOutputStream out1 = new ByteArrayOutputStream();
+ ByteArrayOutputStream out2 = new ByteArrayOutputStream();
+
+ SsFormat.appendCompositeTag(out1, tag1);
+ SsFormat.appendCompositeTag(out2, tag2);
+
+ assertTrue(
+ "Tag " + tag1 + " should encode smaller than tag " + tag2,
+ UNSIGNED_BYTE_COMPARATOR.compare(out1.toByteArray(), out2.toByteArray()) < 0);
+ }
+ }
+ }
+
+ // ==================== Signed Integer Tests ====================
+
+ @Test
+ public void appendIntIncreasing_preservesOrdering() {
+ // Verify that encoded integers maintain their natural ordering
+ for (int i = 0; i < signedIntTestValues.size() - 1; i++) {
+ long v1 = signedIntTestValues.get(i);
+ long v2 = signedIntTestValues.get(i + 1);
+
+ ByteArrayOutputStream out1 = new ByteArrayOutputStream();
+ ByteArrayOutputStream out2 = new ByteArrayOutputStream();
+
+ SsFormat.appendIntIncreasing(out1, v1);
+ SsFormat.appendIntIncreasing(out2, v2);
+
+ assertTrue(
+ "Encoded " + v1 + " should be less than encoded " + v2,
+ UNSIGNED_BYTE_COMPARATOR.compare(out1.toByteArray(), out2.toByteArray()) < 0);
+ }
+ }
+
+ @Test
+ public void appendIntDecreasing_reversesOrdering() {
+ // Verify that decreasing encoding reverses the ordering
+ for (int i = 0; i < signedIntTestValues.size() - 1; i++) {
+ long v1 = signedIntTestValues.get(i);
+ long v2 = signedIntTestValues.get(i + 1);
+
+ ByteArrayOutputStream out1 = new ByteArrayOutputStream();
+ ByteArrayOutputStream out2 = new ByteArrayOutputStream();
+
+ SsFormat.appendIntDecreasing(out1, v1);
+ SsFormat.appendIntDecreasing(out2, v2);
+
+ assertTrue(
+ "Decreasing encoded " + v1 + " should be greater than encoded " + v2,
+ UNSIGNED_BYTE_COMPARATOR.compare(out1.toByteArray(), out2.toByteArray()) > 0);
+ }
+ }
+
+ @Test
+ public void appendIntIncreasing_hasIsKeyBitSet() {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ SsFormat.appendIntIncreasing(out, 42);
+ byte[] result = out.toByteArray();
+
+ assertTrue("IS_KEY bit (0x80) should be set", (result[0] & 0x80) != 0);
+ }
+
+ @Test
+ public void appendIntIncreasing_edgeCases() {
+ long[] edgeCases = {Long.MIN_VALUE, -1, 0, 1, Long.MAX_VALUE};
+
+ for (long value : edgeCases) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ SsFormat.appendIntIncreasing(out, value);
+ byte[] result = out.toByteArray();
+
+ assertTrue("Result should have at least 2 bytes for value " + value, result.length >= 2);
+ assertTrue("IS_KEY bit should be set for value " + value, (result[0] & 0x80) != 0);
+ }
+ }
+
+ // ==================== Unsigned Integer Tests ====================
+
+ @Test
+ public void appendUnsignedIntIncreasing_preservesOrdering() {
+ // Filter to only non-negative values for unsigned comparison
+ List positiveValues = new ArrayList<>();
+ for (long v : unsignedIntTestValues) {
+ if (v >= 0) positiveValues.add(v);
+ }
+ positiveValues.sort(Long::compareUnsigned);
+
+ for (int i = 0; i < positiveValues.size() - 1; i++) {
+ long v1 = positiveValues.get(i);
+ long v2 = positiveValues.get(i + 1);
+
+ ByteArrayOutputStream out1 = new ByteArrayOutputStream();
+ ByteArrayOutputStream out2 = new ByteArrayOutputStream();
+
+ SsFormat.appendUnsignedIntIncreasing(out1, v1);
+ SsFormat.appendUnsignedIntIncreasing(out2, v2);
+
+ assertTrue(
+ "Unsigned encoded "
+ + Long.toUnsignedString(v1)
+ + " should be less than "
+ + Long.toUnsignedString(v2),
+ UNSIGNED_BYTE_COMPARATOR.compare(out1.toByteArray(), out2.toByteArray()) < 0);
+ }
+ }
+
+ @Test
+ public void appendUnsignedIntIncreasing_rejectsNegativeValues() {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ assertThrows(
+ IllegalArgumentException.class, () -> SsFormat.appendUnsignedIntIncreasing(out, -1));
+ }
+
+ @Test
+ public void appendUnsignedIntDecreasing_reversesOrdering() {
+ long[] values = {0, 1, 100, 1000, Long.MAX_VALUE};
+
+ for (int i = 0; i < values.length - 1; i++) {
+ ByteArrayOutputStream out1 = new ByteArrayOutputStream();
+ ByteArrayOutputStream out2 = new ByteArrayOutputStream();
+
+ SsFormat.appendUnsignedIntDecreasing(out1, values[i]);
+ SsFormat.appendUnsignedIntDecreasing(out2, values[i + 1]);
+
+ assertTrue(
+ "Decreasing unsigned encoded " + values[i] + " should be greater than " + values[i + 1],
+ UNSIGNED_BYTE_COMPARATOR.compare(out1.toByteArray(), out2.toByteArray()) > 0);
+ }
+ }
+
+ // ==================== String Tests ====================
+
+ @Test
+ public void appendStringIncreasing_preservesOrdering() {
+ String[] strings = {"", "a", "aa", "ab", "b", "hello", "world", "\u00ff"};
+ Arrays.sort(strings);
+
+ for (int i = 0; i < strings.length - 1; i++) {
+ ByteArrayOutputStream out1 = new ByteArrayOutputStream();
+ ByteArrayOutputStream out2 = new ByteArrayOutputStream();
+
+ SsFormat.appendStringIncreasing(out1, strings[i]);
+ SsFormat.appendStringIncreasing(out2, strings[i + 1]);
+
+ assertTrue(
+ "Encoded '" + strings[i] + "' should be less than '" + strings[i + 1] + "'",
+ UNSIGNED_BYTE_COMPARATOR.compare(out1.toByteArray(), out2.toByteArray()) < 0);
+ }
+ }
+
+ @Test
+ public void appendStringDecreasing_reversesOrdering() {
+ String[] strings = {"", "a", "b", "hello"};
+
+ for (int i = 0; i < strings.length - 1; i++) {
+ ByteArrayOutputStream out1 = new ByteArrayOutputStream();
+ ByteArrayOutputStream out2 = new ByteArrayOutputStream();
+
+ SsFormat.appendStringDecreasing(out1, strings[i]);
+ SsFormat.appendStringDecreasing(out2, strings[i + 1]);
+
+ assertTrue(
+ "Decreasing encoded '" + strings[i] + "' should be greater than '" + strings[i + 1] + "'",
+ UNSIGNED_BYTE_COMPARATOR.compare(out1.toByteArray(), out2.toByteArray()) > 0);
+ }
+ }
+
+ @Test
+ public void appendStringIncreasing_escapesSpecialBytes() {
+ // Test that 0x00 and 0xFF bytes are properly escaped
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ SsFormat.appendBytesIncreasing(out, new byte[] {0x00, (byte) 0xFF, 0x42});
+ byte[] result = out.toByteArray();
+
+ // Result should be longer due to escaping:
+ // header (1) + escaped 0x00 (2) + escaped 0xFF (2) + 0x42 (1) + terminator (2) = 8
+ assertTrue("Result should include escape sequences", result.length > 5);
+ }
+
+ @Test
+ public void appendStringIncreasing_emptyString() {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ SsFormat.appendStringIncreasing(out, "");
+ byte[] result = out.toByteArray();
+
+ // Empty string should still have header + terminator
+ assertTrue("Empty string encoding should have at least 3 bytes", result.length >= 3);
+ assertTrue("IS_KEY bit should be set", (result[0] & 0x80) != 0);
+ }
+
+ // ==================== Bytes Tests ====================
+
+ @Test
+ public void appendBytesIncreasing_preservesOrdering() {
+ byte[][] testBytes = {
+ new byte[] {},
+ new byte[] {0x00},
+ new byte[] {0x01},
+ new byte[] {0x01, 0x02},
+ new byte[] {(byte) 0xFF}
+ };
+
+ for (int i = 0; i < testBytes.length - 1; i++) {
+ ByteArrayOutputStream out1 = new ByteArrayOutputStream();
+ ByteArrayOutputStream out2 = new ByteArrayOutputStream();
+
+ SsFormat.appendBytesIncreasing(out1, testBytes[i]);
+ SsFormat.appendBytesIncreasing(out2, testBytes[i + 1]);
+
+ assertTrue(
+ "Encoded bytes should maintain lexicographic order",
+ UNSIGNED_BYTE_COMPARATOR.compare(out1.toByteArray(), out2.toByteArray()) < 0);
+ }
+ }
+
+ // ==================== Double Tests ====================
+
+ @Test
+ public void appendDoubleIncreasing_preservesOrdering() {
+ // Filter out NaN as it has special comparison semantics
+ List sortedDoubles = new ArrayList<>();
+ for (double d : doubleTestValues) {
+ if (!Double.isNaN(d)) {
+ sortedDoubles.add(d);
+ }
+ }
+ sortedDoubles.sort(Double::compare);
+
+ for (int i = 0; i < sortedDoubles.size() - 1; i++) {
+ double v1 = sortedDoubles.get(i);
+ double v2 = sortedDoubles.get(i + 1);
+
+ ByteArrayOutputStream out1 = new ByteArrayOutputStream();
+ ByteArrayOutputStream out2 = new ByteArrayOutputStream();
+
+ SsFormat.appendDoubleIncreasing(out1, v1);
+ SsFormat.appendDoubleIncreasing(out2, v2);
+
+ int cmp = UNSIGNED_BYTE_COMPARATOR.compare(out1.toByteArray(), out2.toByteArray());
+
+ // Note: -0.0 and 0.0 encode identically (both map to 0 internally), so allow equality
+ assertTrue("Encoded " + v1 + " should be <= encoded " + v2, cmp <= 0);
+ }
+ }
+
+ @Test
+ public void appendDoubleDecreasing_reversesOrdering() {
+ double[] values = {-Double.MAX_VALUE, -1.0, 0.0, 1.0, Double.MAX_VALUE};
+
+ for (int i = 0; i < values.length - 1; i++) {
+ ByteArrayOutputStream out1 = new ByteArrayOutputStream();
+ ByteArrayOutputStream out2 = new ByteArrayOutputStream();
+
+ SsFormat.appendDoubleDecreasing(out1, values[i]);
+ SsFormat.appendDoubleDecreasing(out2, values[i + 1]);
+
+ assertTrue(
+ "Decreasing encoded " + values[i] + " should be greater than " + values[i + 1],
+ UNSIGNED_BYTE_COMPARATOR.compare(out1.toByteArray(), out2.toByteArray()) > 0);
+ }
+ }
+
+ @Test
+ public void appendDoubleIncreasing_specialValues() {
+ // Test special double values
+ // Note: -0.0 is excluded because it encodes identically to 0.0
+ // (both have internal representation mapping to 0)
+ double[] specialValues = {
+ Double.NEGATIVE_INFINITY,
+ -Double.MAX_VALUE,
+ -1.0,
+ -Double.MIN_VALUE,
+ 0.0, // -0.0 encodes the same as 0.0
+ Double.MIN_VALUE,
+ 1.0,
+ Double.MAX_VALUE,
+ Double.POSITIVE_INFINITY
+ };
+
+ // Verify ordering is preserved
+ for (int i = 0; i < specialValues.length - 1; i++) {
+ ByteArrayOutputStream out1 = new ByteArrayOutputStream();
+ ByteArrayOutputStream out2 = new ByteArrayOutputStream();
+
+ SsFormat.appendDoubleIncreasing(out1, specialValues[i]);
+ SsFormat.appendDoubleIncreasing(out2, specialValues[i + 1]);
+
+ assertTrue(
+ "Special value " + specialValues[i] + " should encode less than " + specialValues[i + 1],
+ UNSIGNED_BYTE_COMPARATOR.compare(out1.toByteArray(), out2.toByteArray()) < 0);
+ }
+ }
+
+ @Test
+ public void appendDoubleIncreasing_negativeZeroEqualsPositiveZero() {
+ // Verify that -0.0 and 0.0 encode identically
+ // This is correct behavior: both map to internal representation 0
+ ByteArrayOutputStream outNegZero = new ByteArrayOutputStream();
+ ByteArrayOutputStream outPosZero = new ByteArrayOutputStream();
+
+ SsFormat.appendDoubleIncreasing(outNegZero, -0.0);
+ SsFormat.appendDoubleIncreasing(outPosZero, 0.0);
+
+ assertArrayEquals(
+ "-0.0 and 0.0 should encode identically",
+ outNegZero.toByteArray(),
+ outPosZero.toByteArray());
+ }
+
+ @Test
+ public void appendDoubleIncreasing_nan() {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ SsFormat.appendDoubleIncreasing(out, Double.NaN);
+ byte[] result = out.toByteArray();
+
+ assertTrue("NaN encoding should have at least 2 bytes", result.length >= 2);
+ assertTrue("IS_KEY bit should be set for NaN", (result[0] & 0x80) != 0);
+ }
+
+ // ==================== Null Marker Tests ====================
+
+ @Test
+ public void appendNullOrderedFirst_encoding() {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ SsFormat.appendNullOrderedFirst(out);
+ byte[] result = out.toByteArray();
+
+ assertEquals("Null ordered first should encode to 2 bytes", 2, result.length);
+ assertTrue("IS_KEY bit should be set", (result[0] & 0x80) != 0);
+ }
+
+ @Test
+ public void appendNullOrderedLast_encoding() {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ SsFormat.appendNullOrderedLast(out);
+ byte[] result = out.toByteArray();
+
+ assertEquals("Null ordered last should encode to 2 bytes", 2, result.length);
+ assertTrue("IS_KEY bit should be set", (result[0] & 0x80) != 0);
+ }
+
+ @Test
+ public void appendNotNullMarkerNullOrderedFirst_encoding() {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ SsFormat.appendNotNullMarkerNullOrderedFirst(out);
+ byte[] result = out.toByteArray();
+
+ assertEquals("Not-null marker (nulls first) should encode to 1 byte", 1, result.length);
+ }
+
+ @Test
+ public void appendNotNullMarkerNullOrderedLast_encoding() {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ SsFormat.appendNotNullMarkerNullOrderedLast(out);
+ byte[] result = out.toByteArray();
+
+ assertEquals("Not-null marker (nulls last) should encode to 1 byte", 1, result.length);
+ }
+
+ @Test
+ public void nullOrderedFirst_sortsBeforeValues() {
+ ByteArrayOutputStream nullOut = new ByteArrayOutputStream();
+ ByteArrayOutputStream valueOut = new ByteArrayOutputStream();
+
+ SsFormat.appendNullOrderedFirst(nullOut);
+ SsFormat.appendNotNullMarkerNullOrderedFirst(valueOut);
+ SsFormat.appendIntIncreasing(valueOut, Long.MIN_VALUE);
+
+ assertTrue(
+ "Null (ordered first) should sort before any value",
+ UNSIGNED_BYTE_COMPARATOR.compare(nullOut.toByteArray(), valueOut.toByteArray()) < 0);
+ }
+
+ @Test
+ public void nullOrderedLast_sortsAfterValues() {
+ ByteArrayOutputStream nullOut = new ByteArrayOutputStream();
+ ByteArrayOutputStream valueOut = new ByteArrayOutputStream();
+
+ SsFormat.appendNullOrderedLast(nullOut);
+ SsFormat.appendNotNullMarkerNullOrderedLast(valueOut);
+ SsFormat.appendIntIncreasing(valueOut, Long.MAX_VALUE);
+
+ assertTrue(
+ "Null (ordered last) should sort after any value",
+ UNSIGNED_BYTE_COMPARATOR.compare(nullOut.toByteArray(), valueOut.toByteArray()) > 0);
+ }
+
+ // ==================== Timestamp Tests ====================
+
+ @Test
+ public void encodeTimestamp_length() {
+ byte[] result = SsFormat.encodeTimestamp(0, 0);
+ assertEquals("Timestamp should encode to 12 bytes", 12, result.length);
+ }
+
+ @Test
+ public void encodeTimestamp_preservesOrdering() {
+ long[][] timestamps = {
+ {0, 0},
+ {0, 1},
+ {0, 999999999},
+ {1, 0},
+ {100, 500000000},
+ {Long.MAX_VALUE / 2, 0}
+ };
+
+ for (int i = 0; i < timestamps.length - 1; i++) {
+ byte[] t1 = SsFormat.encodeTimestamp(timestamps[i][0], (int) timestamps[i][1]);
+ byte[] t2 = SsFormat.encodeTimestamp(timestamps[i + 1][0], (int) timestamps[i + 1][1]);
+
+ assertTrue(
+ "Earlier timestamp should encode smaller", UNSIGNED_BYTE_COMPARATOR.compare(t1, t2) < 0);
+ }
+ }
+
+ // ==================== UUID Tests ====================
+
+ @Test
+ public void encodeUuid_length() {
+ byte[] result = SsFormat.encodeUuid(0, 0);
+ assertEquals("UUID should encode to 16 bytes", 16, result.length);
+ }
+
+ @Test
+ public void encodeUuid_bigEndianEncoding() {
+ byte[] result = SsFormat.encodeUuid(0x0102030405060708L, 0x090A0B0C0D0E0F10L);
+
+ // Verify big-endian encoding of high bits
+ assertEquals(0x01, result[0] & 0xFF);
+ assertEquals(0x02, result[1] & 0xFF);
+ assertEquals(0x03, result[2] & 0xFF);
+ assertEquals(0x04, result[3] & 0xFF);
+ assertEquals(0x05, result[4] & 0xFF);
+ assertEquals(0x06, result[5] & 0xFF);
+ assertEquals(0x07, result[6] & 0xFF);
+ assertEquals(0x08, result[7] & 0xFF);
+
+ // Verify big-endian encoding of low bits
+ assertEquals(0x09, result[8] & 0xFF);
+ assertEquals(0x0A, result[9] & 0xFF);
+ assertEquals(0x0B, result[10] & 0xFF);
+ assertEquals(0x0C, result[11] & 0xFF);
+ assertEquals(0x0D, result[12] & 0xFF);
+ assertEquals(0x0E, result[13] & 0xFF);
+ assertEquals(0x0F, result[14] & 0xFF);
+ assertEquals(0x10, result[15] & 0xFF);
+ }
+
+ @Test
+ public void encodeUuid_preservesOrdering() {
+ // UUIDs compared as unsigned 128-bit integers should preserve order
+ long[][] uuids = {
+ {0, 0},
+ {0, 1},
+ {0, Long.MAX_VALUE},
+ {1, 0},
+ {Long.MAX_VALUE, Long.MAX_VALUE}
+ };
+
+ for (int i = 0; i < uuids.length - 1; i++) {
+ byte[] u1 = SsFormat.encodeUuid(uuids[i][0], uuids[i][1]);
+ byte[] u2 = SsFormat.encodeUuid(uuids[i + 1][0], uuids[i + 1][1]);
+
+ assertTrue("UUID ordering should be preserved", UNSIGNED_BYTE_COMPARATOR.compare(u1, u2) < 0);
+ }
+ }
+
+ // ==================== Composite Key Tests ====================
+
+ @Test
+ public void compositeKey_tagPlusIntPreservesOrdering() {
+ int tag = 5;
+ long[] values = {Long.MIN_VALUE, -1, 0, 1, Long.MAX_VALUE};
+
+ for (int i = 0; i < values.length - 1; i++) {
+ ByteArrayOutputStream out1 = new ByteArrayOutputStream();
+ ByteArrayOutputStream out2 = new ByteArrayOutputStream();
+
+ SsFormat.appendCompositeTag(out1, tag);
+ SsFormat.appendIntIncreasing(out1, values[i]);
+
+ SsFormat.appendCompositeTag(out2, tag);
+ SsFormat.appendIntIncreasing(out2, values[i + 1]);
+
+ assertTrue(
+ "Composite key with " + values[i] + " should be less than with " + values[i + 1],
+ UNSIGNED_BYTE_COMPARATOR.compare(out1.toByteArray(), out2.toByteArray()) < 0);
+ }
+ }
+
+ @Test
+ public void compositeKey_differentTagsSortByTag() {
+ long value = 100;
+
+ ByteArrayOutputStream out1 = new ByteArrayOutputStream();
+ ByteArrayOutputStream out2 = new ByteArrayOutputStream();
+
+ SsFormat.appendCompositeTag(out1, 5);
+ SsFormat.appendIntIncreasing(out1, value);
+
+ SsFormat.appendCompositeTag(out2, 10);
+ SsFormat.appendIntIncreasing(out2, value);
+
+ assertTrue(
+ "Key with smaller tag should sort first",
+ UNSIGNED_BYTE_COMPARATOR.compare(out1.toByteArray(), out2.toByteArray()) < 0);
+ }
+
+ @Test
+ public void compositeKey_multipleKeyParts() {
+ // Simulate encoding a composite key with multiple parts: tag + int + string
+ ByteArrayOutputStream out1 = new ByteArrayOutputStream();
+ ByteArrayOutputStream out2 = new ByteArrayOutputStream();
+
+ SsFormat.appendCompositeTag(out1, 1);
+ SsFormat.appendIntIncreasing(out1, 100);
+ SsFormat.appendStringIncreasing(out1, "alice");
+
+ SsFormat.appendCompositeTag(out2, 1);
+ SsFormat.appendIntIncreasing(out2, 100);
+ SsFormat.appendStringIncreasing(out2, "bob");
+
+ assertTrue(
+ "Keys with same prefix but different strings should order by string",
+ UNSIGNED_BYTE_COMPARATOR.compare(out1.toByteArray(), out2.toByteArray()) < 0);
+ }
+
+ // ==================== Order Preservation Summary Test ====================
+
+ @Test
+ public void orderPreservation_comprehensiveIntTest() {
+ // Take a sample of values to avoid O(n^2) test time
+ int step = Math.max(1, signedIntTestValues.size() / 100);
+ List sample = new ArrayList<>();
+ for (int i = 0; i < signedIntTestValues.size(); i += step) {
+ sample.add(signedIntTestValues.get(i));
+ }
+
+ // Encode all values
+ List encoded = new ArrayList<>();
+ for (long v : sample) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ SsFormat.appendIntIncreasing(out, v);
+ encoded.add(out.toByteArray());
+ }
+
+ // Verify the encoded values are in the same order as the original values
+ for (int i = 0; i < sample.size() - 1; i++) {
+ int comparison = UNSIGNED_BYTE_COMPARATOR.compare(encoded.get(i), encoded.get(i + 1));
+ assertTrue(
+ "Order should be preserved: " + sample.get(i) + " < " + sample.get(i + 1),
+ comparison < 0);
+ }
+ }
+}