diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GrowableByteArrayOutputStream.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GrowableByteArrayOutputStream.java
new file mode 100644
index 0000000000..9f9972bfb9
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GrowableByteArrayOutputStream.java
@@ -0,0 +1,105 @@
+/*
+ * 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 java.util.Arrays;
+
+/**
+ * A simple, unsynchronized byte array output stream optimized for key encoding.
+ *
+ *
Unlike {@link java.io.ByteArrayOutputStream}, this class is not thread-safe and does not incur
+ * synchronization overhead. This provides better performance for single-threaded key encoding
+ * operations where synchronization is not required.
+ */
+@InternalApi
+public final class GrowableByteArrayOutputStream {
+
+ private byte[] buf;
+ private int count;
+
+ /** Creates a new output stream with a default initial capacity of 32 bytes. */
+ public GrowableByteArrayOutputStream() {
+ this(32);
+ }
+
+ /**
+ * Creates a new output stream with the specified initial capacity.
+ *
+ * @param initialCapacity the initial buffer size
+ * @throws IllegalArgumentException if initialCapacity is negative
+ */
+ public GrowableByteArrayOutputStream(int initialCapacity) {
+ if (initialCapacity < 0) {
+ throw new IllegalArgumentException("Negative initial capacity: " + initialCapacity);
+ }
+ this.buf = new byte[initialCapacity];
+ }
+
+ private void ensureCapacity(int minCapacity) {
+ if (minCapacity > buf.length) {
+ int newCapacity = Math.max(buf.length << 1, minCapacity);
+ buf = Arrays.copyOf(buf, newCapacity);
+ }
+ }
+
+ /**
+ * Writes the specified byte to this output stream.
+ *
+ * @param b the byte to write (only the low 8 bits are used)
+ */
+ public void write(int b) {
+ ensureCapacity(count + 1);
+ buf[count++] = (byte) b;
+ }
+
+ /**
+ * Writes a portion of a byte array to this output stream.
+ *
+ * @param b the source byte array
+ * @param off the start offset in the array
+ * @param len the number of bytes to write
+ */
+ public void write(byte[] b, int off, int len) {
+ ensureCapacity(count + len);
+ System.arraycopy(b, off, buf, count, len);
+ count += len;
+ }
+
+ /**
+ * Returns a copy of the buffer contents as a new byte array.
+ *
+ * @return a new byte array containing the written bytes
+ */
+ public byte[] toByteArray() {
+ return Arrays.copyOf(buf, count);
+ }
+
+ /** Resets the buffer so that it can be reused. The underlying buffer is retained. */
+ public void reset() {
+ count = 0;
+ }
+
+ /**
+ * Returns the current number of bytes written to this stream.
+ *
+ * @return the number of valid bytes in the buffer
+ */
+ public int size() {
+ return count;
+ }
+}
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..a81ef810ac
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SsFormat.java
@@ -0,0 +1,356 @@
+/*
+ * 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;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Sortable String Format encoding utilities for Spanner keys.
+ *
+ *
This class provides methods to encode various data types into a byte format that preserves
+ * lexicographic ordering. The encoding supports both increasing and decreasing sort orders.
+ */
+@InternalApi
+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();
+ bytes[bytes.length - 1] = (byte) (bytes[bytes.length - 1] | 1);
+ return ByteString.copyFrom(bytes);
+ }
+
+ private SsFormat() {}
+
+ private static final int IS_KEY = 0x80;
+
+ // HeaderType enum values
+ // Unsigned integers (variable length 1-9 bytes)
+ private static final int TYPE_UINT_1 = 0;
+ private static final int TYPE_DECREASING_UINT_1 = 40;
+
+ // Signed integers (variable length 1-8 bytes)
+ private static final int TYPE_NEG_INT_1 = 16;
+ private static final int TYPE_POS_INT_1 = 17;
+ private static final int TYPE_DECREASING_NEG_INT_1 = 48;
+ private static final int TYPE_DECREASING_POS_INT_1 = 49;
+
+ // Strings
+ private static final int TYPE_STRING = 25;
+ private static final int TYPE_DECREASING_STRING = 57;
+
+ // Nullable markers
+ 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_NULLABLE_NOT_NULL_NULL_ORDERED_LAST = 59;
+ private static final int TYPE_NULL_ORDERED_LAST = 60;
+
+ // Doubles (variable length 1-8 bytes, encoded as transformed int64)
+ private static final int TYPE_NEG_DOUBLE_1 = 73;
+ private static final int TYPE_POS_DOUBLE_1 = 74;
+ private static final int TYPE_DECREASING_NEG_DOUBLE_1 = 89;
+ private static final int TYPE_DECREASING_POS_DOUBLE_1 = 90;
+
+ // 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;
+
+ // Offset to make negative timestamp seconds sort correctly
+ private static final long TIMESTAMP_SECONDS_OFFSET = 1L << 63;
+
+ public static void appendCompositeTag(GrowableByteArrayOutputStream 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(GrowableByteArrayOutputStream out) {
+ out.write((byte) (IS_KEY | TYPE_NULL_ORDERED_FIRST));
+ out.write((byte) 0);
+ }
+
+ public static void appendNullOrderedLast(GrowableByteArrayOutputStream out) {
+ out.write((byte) (IS_KEY | TYPE_NULL_ORDERED_LAST));
+ out.write((byte) 0);
+ }
+
+ public static void appendNotNullMarkerNullOrderedFirst(GrowableByteArrayOutputStream out) {
+ out.write((byte) (IS_KEY | TYPE_NULLABLE_NOT_NULL_NULL_ORDERED_FIRST));
+ }
+
+ public static void appendNotNullMarkerNullOrderedLast(GrowableByteArrayOutputStream out) {
+ out.write((byte) (IS_KEY | TYPE_NULLABLE_NOT_NULL_NULL_ORDERED_LAST));
+ }
+
+ /**
+ * Appends an unsigned long value in ascending (increasing) sort order.
+ *
+ *
This encodes a non-negative long value using variable-length encoding that preserves
+ * lexicographic ordering. The encoding uses 1-9 bytes depending on the magnitude of the value.
+ *
+ * @param out the output stream to append to
+ * @param val the unsigned value to encode, must be in range [0, Long.MAX_VALUE]
+ * @throws IllegalArgumentException if val is negative
+ */
+ public static void appendUnsignedLongIncreasing(GrowableByteArrayOutputStream out, long val) {
+ if (val < 0) {
+ throw new IllegalArgumentException(
+ "Unsigned long value must be non-negative: "
+ + val
+ + ". Values requiring the upper half of unsigned 64-bit range are not supported.");
+ }
+ 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)));
+ out.write(buf, 9 - len, len);
+ }
+
+ /**
+ * Appends an unsigned long value in descending (decreasing) sort order.
+ *
+ *
This encodes a non-negative long value using variable-length encoding that preserves reverse
+ * lexicographic ordering. The encoding uses 1-9 bytes depending on the magnitude of the value.
+ *
+ * @param out the output stream to append to
+ * @param val the unsigned value to encode, must be in range [0, Long.MAX_VALUE]
+ * @throws IllegalArgumentException if val is negative
+ */
+ public static void appendUnsignedLongDecreasing(GrowableByteArrayOutputStream out, long val) {
+ if (val < 0) {
+ throw new IllegalArgumentException(
+ "Unsigned long value must be non-negative: "
+ + val
+ + ". Values requiring the upper half of unsigned 64-bit range are not supported.");
+ }
+ byte[] buf = new byte[9];
+ int len = 0;
+ long tempVal = val;
+
+ buf[8 - len] = (byte) ((~(tempVal & 0x7F) & 0x7F) << 1);
+ tempVal >>= 7;
+ len++;
+
+ while (tempVal > 0) {
+ buf[8 - len] = (byte) (~(tempVal & 0xFF));
+ tempVal >>= 8;
+ len++;
+ }
+
+ out.write((byte) (IS_KEY | (TYPE_DECREASING_UINT_1 - len + 1)));
+ out.write(buf, 9 - len, len);
+ }
+
+ private static void appendIntInternal(
+ GrowableByteArrayOutputStream 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) {
+ // Defensive assertion: unreachable for any valid 64-bit signed integer
+ 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));
+ out.write(buf, 8 - len, len);
+ }
+
+ public static void appendIntIncreasing(GrowableByteArrayOutputStream out, long value) {
+ appendIntInternal(out, value, false, false);
+ }
+
+ public static void appendIntDecreasing(GrowableByteArrayOutputStream out, long value) {
+ appendIntInternal(out, value, true, false);
+ }
+
+ public static void appendDoubleIncreasing(GrowableByteArrayOutputStream 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(GrowableByteArrayOutputStream 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(
+ GrowableByteArrayOutputStream 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) {
+ // Escape sequence for 0x00: write 0x00 followed by 0xF0
+ out.write((byte) 0x00);
+ out.write(ASCENDING_ZERO_ESCAPE);
+ } else if (unsignedByte == 0xFF) {
+ // Escape sequence for 0xFF: write 0xFF followed by 0x10
+ out.write((byte) 0xFF);
+ out.write(ASCENDING_FF_ESCAPE);
+ } else {
+ out.write((byte) unsignedByte);
+ }
+ }
+ // Terminator
+ out.write((byte) (decreasing ? 0xFF : 0x00));
+ out.write(SEP);
+ }
+
+ public static void appendStringIncreasing(GrowableByteArrayOutputStream out, String value) {
+ appendByteSequence(out, value.getBytes(StandardCharsets.UTF_8), false);
+ }
+
+ public static void appendStringDecreasing(GrowableByteArrayOutputStream out, String value) {
+ appendByteSequence(out, value.getBytes(StandardCharsets.UTF_8), true);
+ }
+
+ public static void appendBytesIncreasing(GrowableByteArrayOutputStream out, byte[] value) {
+ appendByteSequence(out, value, false);
+ }
+
+ public static void appendBytesDecreasing(GrowableByteArrayOutputStream 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) {
+ long offsetSeconds = seconds + TIMESTAMP_SECONDS_OFFSET;
+ byte[] buf = new byte[12];
+ ByteBuffer.wrap(buf).order(ByteOrder.BIG_ENDIAN).putLong(offsetSeconds).putInt(nanos);
+ 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];
+ ByteBuffer.wrap(buf).order(ByteOrder.BIG_ENDIAN).putLong(high).putLong(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..492fcdeaeb
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/SsFormatTest.java
@@ -0,0 +1,899 @@
+/*
+ * 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.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import com.google.protobuf.ByteString;
+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++) {
+ GrowableByteArrayOutputStream out = new GrowableByteArrayOutputStream();
+ 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) {
+ GrowableByteArrayOutputStream out = new GrowableByteArrayOutputStream();
+ 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) {
+ GrowableByteArrayOutputStream out = new GrowableByteArrayOutputStream();
+ 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() {
+ GrowableByteArrayOutputStream out = new GrowableByteArrayOutputStream();
+ 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++) {
+ GrowableByteArrayOutputStream out1 = new GrowableByteArrayOutputStream();
+ GrowableByteArrayOutputStream out2 = new GrowableByteArrayOutputStream();
+
+ 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);
+
+ GrowableByteArrayOutputStream out1 = new GrowableByteArrayOutputStream();
+ GrowableByteArrayOutputStream out2 = new GrowableByteArrayOutputStream();
+
+ 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);
+
+ GrowableByteArrayOutputStream out1 = new GrowableByteArrayOutputStream();
+ GrowableByteArrayOutputStream out2 = new GrowableByteArrayOutputStream();
+
+ 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() {
+ GrowableByteArrayOutputStream out = new GrowableByteArrayOutputStream();
+ 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) {
+ GrowableByteArrayOutputStream out = new GrowableByteArrayOutputStream();
+ 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 appendUnsignedLongIncreasing_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);
+
+ GrowableByteArrayOutputStream out1 = new GrowableByteArrayOutputStream();
+ GrowableByteArrayOutputStream out2 = new GrowableByteArrayOutputStream();
+
+ SsFormat.appendUnsignedLongIncreasing(out1, v1);
+ SsFormat.appendUnsignedLongIncreasing(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 appendUnsignedLongIncreasing_rejectsNegativeValues() {
+ GrowableByteArrayOutputStream out = new GrowableByteArrayOutputStream();
+ assertThrows(
+ IllegalArgumentException.class, () -> SsFormat.appendUnsignedLongIncreasing(out, -1));
+ }
+
+ @Test
+ public void appendUnsignedLongDecreasing_reversesOrdering() {
+ long[] values = {0, 1, 100, 1000, Long.MAX_VALUE};
+
+ for (int i = 0; i < values.length - 1; i++) {
+ GrowableByteArrayOutputStream out1 = new GrowableByteArrayOutputStream();
+ GrowableByteArrayOutputStream out2 = new GrowableByteArrayOutputStream();
+
+ SsFormat.appendUnsignedLongDecreasing(out1, values[i]);
+ SsFormat.appendUnsignedLongDecreasing(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++) {
+ GrowableByteArrayOutputStream out1 = new GrowableByteArrayOutputStream();
+ GrowableByteArrayOutputStream out2 = new GrowableByteArrayOutputStream();
+
+ 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++) {
+ GrowableByteArrayOutputStream out1 = new GrowableByteArrayOutputStream();
+ GrowableByteArrayOutputStream out2 = new GrowableByteArrayOutputStream();
+
+ 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
+ GrowableByteArrayOutputStream out = new GrowableByteArrayOutputStream();
+ 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() {
+ GrowableByteArrayOutputStream out = new GrowableByteArrayOutputStream();
+ 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++) {
+ GrowableByteArrayOutputStream out1 = new GrowableByteArrayOutputStream();
+ GrowableByteArrayOutputStream out2 = new GrowableByteArrayOutputStream();
+
+ 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);
+ }
+ }
+
+ @Test
+ public void appendBytesDecreasing_reversesOrdering() {
+ 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++) {
+ GrowableByteArrayOutputStream out1 = new GrowableByteArrayOutputStream();
+ GrowableByteArrayOutputStream out2 = new GrowableByteArrayOutputStream();
+
+ SsFormat.appendBytesDecreasing(out1, testBytes[i]);
+ SsFormat.appendBytesDecreasing(out2, testBytes[i + 1]);
+
+ assertTrue(
+ "Decreasing encoded bytes should reverse lexicographic order",
+ UNSIGNED_BYTE_COMPARATOR.compare(out1.toByteArray(), out2.toByteArray()) > 0);
+ }
+ }
+
+ @Test
+ public void appendBytesDecreasing_escapesSpecialBytes() {
+ // Test that 0x00 and 0xFF bytes are properly escaped in decreasing mode
+ GrowableByteArrayOutputStream out = new GrowableByteArrayOutputStream();
+ SsFormat.appendBytesDecreasing(out, new byte[] {0x00, (byte) 0xFF, 0x42});
+ byte[] result = out.toByteArray();
+
+ // Result should be longer due to escaping
+ // In decreasing mode: bytes are inverted, then escaped
+ // Original 0x00 -> inverted to 0xFF -> needs escape (0xFF, 0x10)
+ // Original 0xFF -> inverted to 0x00 -> needs escape (0x00, 0xF0)
+ // Original 0x42 -> inverted to 0xBD -> no escape needed
+ assertTrue("Result should include escape sequences", result.length > 5);
+ }
+
+ @Test
+ public void appendBytesDecreasing_emptyArray() {
+ GrowableByteArrayOutputStream out = new GrowableByteArrayOutputStream();
+ SsFormat.appendBytesDecreasing(out, new byte[] {});
+ byte[] result = out.toByteArray();
+
+ // Empty bytes should still have header + terminator
+ assertTrue("Empty bytes encoding should have at least 3 bytes", result.length >= 3);
+ assertTrue("IS_KEY bit should be set", (result[0] & 0x80) != 0);
+ }
+
+ @Test
+ public void appendBytesIncreasing_vs_Decreasing_sameInput_differentOutput() {
+ byte[] input = new byte[] {0x01, 0x02, 0x03};
+
+ GrowableByteArrayOutputStream outInc = new GrowableByteArrayOutputStream();
+ GrowableByteArrayOutputStream outDec = new GrowableByteArrayOutputStream();
+
+ SsFormat.appendBytesIncreasing(outInc, input);
+ SsFormat.appendBytesDecreasing(outDec, input);
+
+ // The outputs should be different (different header type and inverted bytes)
+ assertFalse(
+ "Increasing and decreasing encodings should differ",
+ Arrays.equals(outInc.toByteArray(), outDec.toByteArray()));
+ }
+
+ // ==================== 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);
+
+ GrowableByteArrayOutputStream out1 = new GrowableByteArrayOutputStream();
+ GrowableByteArrayOutputStream out2 = new GrowableByteArrayOutputStream();
+
+ 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++) {
+ GrowableByteArrayOutputStream out1 = new GrowableByteArrayOutputStream();
+ GrowableByteArrayOutputStream out2 = new GrowableByteArrayOutputStream();
+
+ 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++) {
+ GrowableByteArrayOutputStream out1 = new GrowableByteArrayOutputStream();
+ GrowableByteArrayOutputStream out2 = new GrowableByteArrayOutputStream();
+
+ 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
+ GrowableByteArrayOutputStream outNegZero = new GrowableByteArrayOutputStream();
+ GrowableByteArrayOutputStream outPosZero = new GrowableByteArrayOutputStream();
+
+ 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() {
+ GrowableByteArrayOutputStream out = new GrowableByteArrayOutputStream();
+ 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() {
+ GrowableByteArrayOutputStream out = new GrowableByteArrayOutputStream();
+ 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() {
+ GrowableByteArrayOutputStream out = new GrowableByteArrayOutputStream();
+ 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() {
+ GrowableByteArrayOutputStream out = new GrowableByteArrayOutputStream();
+ 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() {
+ GrowableByteArrayOutputStream out = new GrowableByteArrayOutputStream();
+ 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() {
+ GrowableByteArrayOutputStream nullOut = new GrowableByteArrayOutputStream();
+ GrowableByteArrayOutputStream valueOut = new GrowableByteArrayOutputStream();
+
+ 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() {
+ GrowableByteArrayOutputStream nullOut = new GrowableByteArrayOutputStream();
+ GrowableByteArrayOutputStream valueOut = new GrowableByteArrayOutputStream();
+
+ 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++) {
+ GrowableByteArrayOutputStream out1 = new GrowableByteArrayOutputStream();
+ GrowableByteArrayOutputStream out2 = new GrowableByteArrayOutputStream();
+
+ 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;
+
+ GrowableByteArrayOutputStream out1 = new GrowableByteArrayOutputStream();
+ GrowableByteArrayOutputStream out2 = new GrowableByteArrayOutputStream();
+
+ 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
+ GrowableByteArrayOutputStream out1 = new GrowableByteArrayOutputStream();
+ GrowableByteArrayOutputStream out2 = new GrowableByteArrayOutputStream();
+
+ 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) {
+ GrowableByteArrayOutputStream out = new GrowableByteArrayOutputStream();
+ 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);
+ }
+ }
+}