Skip to content
This repository was archived by the owner on Apr 7, 2026. It is now read-only.

Commit 77c4f60

Browse files
committed
feat: add SsFormat encoding library for SpanFE bypass
This commit adds the foundational SsFormat class that provides sortable string format (ssformat) encoding utilities. This encoding is used by Spanner for key ordering and routing. Key features: - Composite tag encoding for interleaved tables - Signed/unsigned integer encoding (increasing/decreasing) - String and bytes encoding with proper escaping - Double encoding with proper sign handling - Timestamp and UUID encoding - Null value markers with configurable ordering - TargetRange class for key range representation This is part of the SpanFE bypass feature that enables location-aware routing for improved latency.
1 parent e3fa634 commit 77c4f60

2 files changed

Lines changed: 395 additions & 0 deletions

File tree

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
package com.google.cloud.spanner.spi.v1;
2+
3+
import com.google.protobuf.ByteString;
4+
import java.io.ByteArrayOutputStream;
5+
import java.nio.charset.StandardCharsets;
6+
7+
public final class SsFormat {
8+
9+
/**
10+
* Makes the given key a prefix successor. This means that the returned key is the smallest
11+
* possible key that is larger than the input key, and that does not have the input key as a
12+
* prefix.
13+
*
14+
* <p>This is done by flipping the least significant bit of the last byte of the key.
15+
*
16+
* @param key The key to make a prefix successor.
17+
* @return The prefix successor key.
18+
*/
19+
public static ByteString makePrefixSuccessor(ByteString key) {
20+
if (key == null || key.isEmpty()) {
21+
return ByteString.EMPTY;
22+
}
23+
byte[] bytes = key.toByteArray();
24+
if (bytes.length > 0) {
25+
bytes[bytes.length - 1] = (byte) (bytes[bytes.length - 1] | 1);
26+
}
27+
return ByteString.copyFrom(bytes);
28+
}
29+
30+
private SsFormat() {}
31+
32+
// Constants from ssformat.cc
33+
private static final int IS_KEY = 0x80;
34+
private static final int TYPE_MASK = 0x7f;
35+
36+
// HeaderType enum values (selected)
37+
private static final int TYPE_UINT_1 = 0;
38+
private static final int TYPE_UINT_9 = 8;
39+
private static final int TYPE_NEG_INT_8 = 9;
40+
private static final int TYPE_NEG_INT_1 = 16;
41+
private static final int TYPE_POS_INT_1 = 17;
42+
private static final int TYPE_POS_INT_8 = 24;
43+
private static final int TYPE_STRING = 25;
44+
private static final int TYPE_NULL_ORDERED_FIRST = 27;
45+
private static final int TYPE_NULLABLE_NOT_NULL_NULL_ORDERED_FIRST = 28;
46+
private static final int TYPE_DECREASING_UINT_9 = 32;
47+
private static final int TYPE_DECREASING_UINT_1 = 40;
48+
private static final int TYPE_DECREASING_NEG_INT_8 = 41;
49+
private static final int TYPE_DECREASING_NEG_INT_1 = 48;
50+
private static final int TYPE_DECREASING_POS_INT_1 = 49;
51+
private static final int TYPE_DECREASING_POS_INT_8 = 56;
52+
private static final int TYPE_DECREASING_STRING = 57;
53+
private static final int TYPE_NULLABLE_NOT_NULL_NULL_ORDERED_LAST = 59;
54+
private static final int TYPE_NULL_ORDERED_LAST = 60;
55+
private static final int TYPE_NEG_DOUBLE_8 = 66;
56+
private static final int TYPE_NEG_DOUBLE_1 = 73;
57+
private static final int TYPE_POS_DOUBLE_1 = 74;
58+
private static final int TYPE_POS_DOUBLE_8 = 81;
59+
private static final int TYPE_DECREASING_NEG_DOUBLE_8 = 82;
60+
private static final int TYPE_DECREASING_NEG_DOUBLE_1 = 89;
61+
private static final int TYPE_DECREASING_POS_DOUBLE_1 = 90;
62+
private static final int TYPE_DECREASING_POS_DOUBLE_8 = 97;
63+
64+
// EscapeChar enum values
65+
private static final byte ASCENDING_ZERO_ESCAPE = (byte) 0xf0;
66+
private static final byte ASCENDING_FF_ESCAPE = (byte) 0x10;
67+
private static final byte SEP = (byte) 0x78; // 'x'
68+
69+
// For AppendCompositeTag
70+
private static final int K_OBJECT_EXISTENCE_TAG = 0x7e;
71+
private static final int K_MAX_FIELD_TAG = 0xffff;
72+
73+
public static void appendCompositeTag(ByteArrayOutputStream out, int tag) {
74+
if (tag == K_OBJECT_EXISTENCE_TAG || tag <= 0 || tag > K_MAX_FIELD_TAG) {
75+
throw new IllegalArgumentException("Invalid tag value: " + tag);
76+
}
77+
78+
if (tag < 16) {
79+
// Short tag: 000 TTTT S (S is LSB of tag, but here tag is original, so S=0)
80+
// Encodes as (tag << 1)
81+
out.write((byte) (tag << 1));
82+
} else {
83+
// Long tag
84+
int shiftedTag = tag << 1; // LSB is 0 for prefix successor
85+
if (shiftedTag < (1 << (5 + 8))) { // Original tag < 4096
86+
// Header: num_extra_bytes=1 (01xxxxx), P=payload bits from tag
87+
// (1 << 5) is 00100000
88+
// (shiftedTag >> 8) are the 5 MSBs of the payload part of the tag
89+
out.write((byte) ((1 << 5) | (shiftedTag >> 8)));
90+
out.write((byte) (shiftedTag & 0xFF));
91+
} else { // Original tag >= 4096 and <= K_MAX_FIELD_TAG (65535)
92+
// Header: num_extra_bytes=2 (10xxxxx)
93+
// (2 << 5) is 01000000
94+
out.write((byte) ((2 << 5) | (shiftedTag >> 16)));
95+
out.write((byte) ((shiftedTag >> 8) & 0xFF));
96+
out.write((byte) (shiftedTag & 0xFF));
97+
}
98+
}
99+
}
100+
101+
public static void appendNullOrderedFirst(ByteArrayOutputStream out) {
102+
out.write((byte) (IS_KEY | TYPE_NULL_ORDERED_FIRST));
103+
out.write((byte) 0);
104+
}
105+
106+
public static void appendNullOrderedLast(ByteArrayOutputStream out) {
107+
out.write((byte) (IS_KEY | TYPE_NULL_ORDERED_LAST));
108+
out.write((byte) 0);
109+
}
110+
111+
public static void appendNotNullMarkerNullOrderedFirst(ByteArrayOutputStream out) {
112+
out.write((byte) (IS_KEY | TYPE_NULLABLE_NOT_NULL_NULL_ORDERED_FIRST));
113+
}
114+
115+
public static void appendNotNullMarkerNullOrderedLast(ByteArrayOutputStream out) {
116+
out.write((byte) (IS_KEY | TYPE_NULLABLE_NOT_NULL_NULL_ORDERED_LAST));
117+
}
118+
119+
public static void appendUnsignedIntIncreasing(ByteArrayOutputStream out, long val) {
120+
if (val < 0) {
121+
throw new IllegalArgumentException("Unsigned int cannot be negative: " + val);
122+
}
123+
byte[] buf = new byte[9]; // Max 9 bytes for value payload
124+
int len = 0;
125+
126+
long tempVal = val;
127+
buf[8 - len] = (byte) ((tempVal & 0x7F) << 1); // LSB is prefix-successor bit (0)
128+
tempVal >>= 7;
129+
len++;
130+
131+
while (tempVal > 0) {
132+
buf[8 - len] = (byte) (tempVal & 0xFF);
133+
tempVal >>= 8;
134+
len++;
135+
}
136+
137+
out.write((byte) (IS_KEY | (TYPE_UINT_1 + len - 1)));
138+
for (int i = 0; i < len; i++) {
139+
out.write((byte) (buf[8 - len + 1 + i] & 0xFF));
140+
}
141+
}
142+
143+
public static void appendUnsignedIntDecreasing(ByteArrayOutputStream out, long val) {
144+
if (val < 0) {
145+
throw new IllegalArgumentException("Unsigned int cannot be negative: " + val);
146+
}
147+
byte[] buf = new byte[9];
148+
int len = 0;
149+
long tempVal = val;
150+
151+
// InvertByte(val & 0x7f) << 1
152+
buf[8 - len] = (byte) ((~(tempVal & 0x7F) & 0x7F) << 1);
153+
tempVal >>= 7;
154+
len++;
155+
156+
while (tempVal > 0) {
157+
buf[8 - len] = (byte) (~(tempVal & 0xFF));
158+
tempVal >>= 8;
159+
len++;
160+
}
161+
// If val was 0, loop doesn't run for len > 1. If len is still 1, all bits of tempVal (0) are
162+
// covered.
163+
// If val was large, but remaining tempVal became 0, this is correct.
164+
// If tempVal was 0 initially, buf[8] has (~0 & 0x7f) << 1. len = 1.
165+
// If tempVal was >0 but became 0 after some shifts, buf[8-len] has inverted last byte.
166+
167+
out.write((byte) (IS_KEY | (TYPE_DECREASING_UINT_1 - len + 1)));
168+
for (int i = 0; i < len; i++) {
169+
out.write((byte) (buf[8 - len + 1 + i] & 0xFF));
170+
}
171+
}
172+
173+
private static void appendIntInternal(
174+
ByteArrayOutputStream out, long val, boolean decreasing, boolean isDouble) {
175+
if (decreasing) {
176+
val = ~val;
177+
}
178+
179+
byte[] buf = new byte[8]; // Max 8 bytes for payload
180+
int len = 0;
181+
long tempVal = val;
182+
183+
if (tempVal >= 0) {
184+
buf[7 - len] = (byte) ((tempVal & 0x7F) << 1);
185+
tempVal >>= 7;
186+
len++;
187+
while (tempVal > 0) {
188+
buf[7 - len] = (byte) (tempVal & 0xFF);
189+
tempVal >>= 8;
190+
len++;
191+
}
192+
} else { // tempVal < 0
193+
// For negative numbers, extend sign bit after shifting
194+
buf[7 - len] = (byte) ((tempVal & 0x7F) << 1);
195+
// Simulate sign extension for right shift of negative number
196+
// (x >> 7) | 0xFE00000000000000ULL; (if x has 64 bits)
197+
// In Java, right shift `>>` on negative longs performs sign extension.
198+
tempVal >>= 7;
199+
len++;
200+
while (tempVal != -1L) { // Loop until all remaining bits are 1s (sign extension)
201+
buf[7 - len] = (byte) (tempVal & 0xFF);
202+
tempVal >>= 8;
203+
len++;
204+
if (len > 8) throw new AssertionError("Signed int encoding overflow");
205+
}
206+
}
207+
208+
int type;
209+
if (val >= 0) { // Original val before potential bit-negation for decreasing
210+
if (!decreasing) {
211+
type = isDouble ? (TYPE_POS_DOUBLE_1 + len - 1) : (TYPE_POS_INT_1 + len - 1);
212+
} else {
213+
type =
214+
isDouble
215+
? (TYPE_DECREASING_POS_DOUBLE_1 + len - 1)
216+
: (TYPE_DECREASING_POS_INT_1 + len - 1);
217+
}
218+
} else {
219+
if (!decreasing) {
220+
type = isDouble ? (TYPE_NEG_DOUBLE_1 - len + 1) : (TYPE_NEG_INT_1 - len + 1);
221+
} else {
222+
type =
223+
isDouble
224+
? (TYPE_DECREASING_NEG_DOUBLE_1 - len + 1)
225+
: (TYPE_DECREASING_NEG_INT_1 - len + 1);
226+
}
227+
}
228+
out.write((byte) (IS_KEY | type));
229+
for (int i = 0; i < len; i++) {
230+
out.write((byte) (buf[7 - len + 1 + i] & 0xFF));
231+
}
232+
}
233+
234+
public static void appendIntIncreasing(ByteArrayOutputStream out, long value) {
235+
appendIntInternal(out, value, false, false);
236+
}
237+
238+
public static void appendIntDecreasing(ByteArrayOutputStream out, long value) {
239+
appendIntInternal(out, value, true, false);
240+
}
241+
242+
public static void appendDoubleIncreasing(ByteArrayOutputStream out, double value) {
243+
long enc = Double.doubleToRawLongBits(value);
244+
if (enc < 0) {
245+
enc =
246+
Long.MIN_VALUE
247+
- enc; // kint64min - enc (equivalent to ~enc for negative values due to 2's
248+
// complement)
249+
}
250+
appendIntInternal(out, enc, false, true);
251+
}
252+
253+
public static void appendDoubleDecreasing(ByteArrayOutputStream out, double value) {
254+
long enc = Double.doubleToRawLongBits(value);
255+
if (enc < 0) {
256+
enc = Long.MIN_VALUE - enc;
257+
}
258+
appendIntInternal(out, enc, true, true);
259+
}
260+
261+
private static void appendByteSequence(
262+
ByteArrayOutputStream out, byte[] bytes, boolean decreasing) {
263+
out.write((byte) (IS_KEY | (decreasing ? TYPE_DECREASING_STRING : TYPE_STRING)));
264+
265+
for (byte b : bytes) {
266+
byte currentByte = decreasing ? (byte) ~b : b;
267+
int unsignedByte = currentByte & 0xFF;
268+
if (unsignedByte == 0x00) {
269+
out.write((byte) 0x00);
270+
out.write(
271+
decreasing
272+
? ASCENDING_ZERO_ESCAPE
273+
: ASCENDING_ZERO_ESCAPE); // After inversion, 0xFF becomes 0x00. Escape for 0x00
274+
// (inverted) is F0.
275+
// If increasing, 0x00 -> 0x00 F0.
276+
} else if (unsignedByte == 0xFF) {
277+
out.write((byte) 0xFF);
278+
out.write(
279+
decreasing
280+
? ASCENDING_FF_ESCAPE
281+
: ASCENDING_FF_ESCAPE); // After inversion, 0x00 becomes 0xFF. Escape for 0xFF
282+
// (inverted) is 0x10.
283+
// If increasing, 0xFF -> 0xFF 0x10.
284+
} else {
285+
out.write((byte) unsignedByte);
286+
}
287+
}
288+
// Terminator
289+
out.write((byte) (decreasing ? 0xFF : 0x00));
290+
out.write(SEP);
291+
}
292+
293+
public static void appendStringIncreasing(ByteArrayOutputStream out, String value) {
294+
appendByteSequence(out, value.getBytes(StandardCharsets.UTF_8), false);
295+
}
296+
297+
public static void appendStringDecreasing(ByteArrayOutputStream out, String value) {
298+
appendByteSequence(out, value.getBytes(StandardCharsets.UTF_8), true);
299+
}
300+
301+
public static void appendBytesIncreasing(ByteArrayOutputStream out, byte[] value) {
302+
appendByteSequence(out, value, false);
303+
}
304+
305+
public static void appendBytesDecreasing(ByteArrayOutputStream out, byte[] value) {
306+
appendByteSequence(out, value, true);
307+
}
308+
309+
/**
310+
* Encodes a timestamp as 12 bytes: 8 bytes for seconds since epoch (with offset to handle
311+
* negative), 4 bytes for nanoseconds.
312+
*/
313+
public static byte[] encodeTimestamp(long seconds, int nanos) {
314+
// Add offset to make negative seconds sort correctly
315+
long kSecondsOffset = 1L << 63;
316+
long hi = seconds + kSecondsOffset;
317+
int lo = nanos;
318+
319+
byte[] buf = new byte[12];
320+
// Big-endian encoding
321+
buf[0] = (byte) (hi >> 56);
322+
buf[1] = (byte) (hi >> 48);
323+
buf[2] = (byte) (hi >> 40);
324+
buf[3] = (byte) (hi >> 32);
325+
buf[4] = (byte) (hi >> 24);
326+
buf[5] = (byte) (hi >> 16);
327+
buf[6] = (byte) (hi >> 8);
328+
buf[7] = (byte) hi;
329+
buf[8] = (byte) (lo >> 24);
330+
buf[9] = (byte) (lo >> 16);
331+
buf[10] = (byte) (lo >> 8);
332+
buf[11] = (byte) lo;
333+
return buf;
334+
}
335+
336+
/** Encodes a UUID (128-bit) as 16 bytes in big-endian order. */
337+
public static byte[] encodeUuid(long high, long low) {
338+
byte[] buf = new byte[16];
339+
// Big-endian encoding
340+
buf[0] = (byte) (high >> 56);
341+
buf[1] = (byte) (high >> 48);
342+
buf[2] = (byte) (high >> 40);
343+
buf[3] = (byte) (high >> 32);
344+
buf[4] = (byte) (high >> 24);
345+
buf[5] = (byte) (high >> 16);
346+
buf[6] = (byte) (high >> 8);
347+
buf[7] = (byte) high;
348+
buf[8] = (byte) (low >> 56);
349+
buf[9] = (byte) (low >> 48);
350+
buf[10] = (byte) (low >> 40);
351+
buf[11] = (byte) (low >> 32);
352+
buf[12] = (byte) (low >> 24);
353+
buf[13] = (byte) (low >> 16);
354+
buf[14] = (byte) (low >> 8);
355+
buf[15] = (byte) low;
356+
return buf;
357+
}
358+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.google.cloud.spanner.spi.v1;
2+
3+
import com.google.protobuf.ByteString;
4+
5+
public class TargetRange {
6+
public ByteString start;
7+
public ByteString limit;
8+
public boolean approximate;
9+
10+
public TargetRange(ByteString start, ByteString limit, boolean approximate) {
11+
this.start = start;
12+
this.limit = limit;
13+
this.approximate = approximate;
14+
}
15+
16+
public boolean isPoint() {
17+
return limit.isEmpty();
18+
}
19+
20+
/**
21+
* Merges another TargetRange into this one. The resulting range will be the union of the two.
22+
* This logic is a direct port of the C++ implementation in `recipe.cc`.
23+
*/
24+
public void mergeFrom(TargetRange other) {
25+
if (ByteString.unsignedLexicographicalComparator().compare(other.start, this.start) < 0) {
26+
this.start = other.start;
27+
}
28+
if (other.isPoint()
29+
&& ByteString.unsignedLexicographicalComparator().compare(other.start, this.limit) >= 0) {
30+
this.limit = SsFormat.makePrefixSuccessor(other.start);
31+
} else if (ByteString.unsignedLexicographicalComparator().compare(other.limit, this.limit)
32+
> 0) {
33+
this.limit = other.limit;
34+
}
35+
this.approximate |= other.approximate;
36+
}
37+
}

0 commit comments

Comments
 (0)