diff --git a/README.md b/README.md
index 415e4e67da..881d3b351e 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ If you are using Maven with [BOM][libraries-bom], add this to your pom.xml file:
com.google.cloud
libraries-bom
- 26.72.0
+ 26.74.0
pom
import
@@ -41,7 +41,7 @@ If you are using Maven without the BOM, add this to your dependencies:
com.google.cloud
google-cloud-spanner
- 6.102.1
+ 6.107.0
```
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/ChannelEndpointCache.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/ChannelEndpointCache.java
index 5329856285..879ed546f2 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/ChannelEndpointCache.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/ChannelEndpointCache.java
@@ -43,19 +43,19 @@ public interface ChannelEndpointCache {
ChannelEndpoint defaultChannel();
/**
- * Returns a cached server for the given address, creating it if needed.
+ * Returns a cached channel for the given address, creating it if needed.
*
- *
If a server for this address already exists in the cache, the cached instance is returned.
+ *
If a channel for this address already exists in the cache, the cached instance is returned.
* Otherwise, a new server connection is created and cached.
*
* @param address the server address in "host:port" format
- * @return a server instance for the address, never null
+ * @return a channel instance for the address, never null
* @throws com.google.cloud.spanner.SpannerException if the channel cannot be created
*/
ChannelEndpoint get(String address);
/**
- * Evicts a server from the cache and gracefully shuts down its channel.
+ * Evicts a server connection from the cache and gracefully shuts down its channel.
*
*
This method should be called when a server becomes unhealthy or is no longer needed. The
* channel shutdown is graceful: existing RPCs are allowed to complete, but new RPCs will not be
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/KeyRecipe.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/KeyRecipe.java
new file mode 100644
index 0000000000..876a68583e
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/KeyRecipe.java
@@ -0,0 +1,845 @@
+/*
+ * 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 com.google.protobuf.ListValue;
+import com.google.protobuf.Struct;
+import com.google.protobuf.Value;
+import com.google.spanner.v1.KeyRange;
+import com.google.spanner.v1.KeySet;
+import com.google.spanner.v1.Mutation;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.format.ResolverStyle;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.function.BiFunction;
+import java.util.stream.Collectors;
+
+@InternalApi
+public final class KeyRecipe {
+
+ // kInfinity is "\xff" - the largest single byte, used as a sentinel for ranges
+ private static final ByteString K_INFINITY = ByteString.copyFrom(new byte[] {(byte) 0xFF});
+
+ private enum Kind {
+ TAG,
+ VALUE,
+ INVALID
+ }
+
+ private enum KeyType {
+ FULL_KEY,
+ PREFIX,
+ PREFIX_SUCCESSOR,
+ INDEX_KEY
+ }
+
+ private enum EncodeState {
+ OK,
+ FAILED,
+ END_OF_KEYS
+ }
+
+ private static final class Part {
+ private final Kind kind;
+ private final int tag; // if kind == TAG
+ private final com.google.spanner.v1.Type type; // if kind == VALUE
+ private final com.google.spanner.v1.KeyRecipe.Part.Order order; // if kind == VALUE
+ private final com.google.spanner.v1.KeyRecipe.Part.NullOrder nullOrder; // if kind == VALUE
+ private final String identifier; // if kind == VALUE
+ private final List structIdentifiers; // if kind == VALUE
+ private final Value constantValue; // if kind == VALUE and value is set
+ private final boolean random; // if kind == VALUE and random: true
+
+ private Value constantValue() {
+ return constantValue;
+ }
+
+ private boolean hasConstantValue() {
+ return constantValue != null;
+ }
+
+ private Part(
+ Kind kind,
+ int tag,
+ com.google.spanner.v1.Type type,
+ com.google.spanner.v1.KeyRecipe.Part.Order order,
+ com.google.spanner.v1.KeyRecipe.Part.NullOrder nullOrder,
+ String identifier,
+ List structIdentifiers,
+ Value constantValue,
+ boolean random) {
+ this.kind = kind;
+ this.tag = tag;
+ this.type = type;
+ this.order = order;
+ this.nullOrder = nullOrder;
+ this.identifier = identifier;
+ this.structIdentifiers = structIdentifiers;
+ this.constantValue = constantValue;
+ this.random = random;
+ }
+
+ private ResolvedValue resolveValue(BiFunction valueFinder, int index) {
+ if (hasConstantValue()) {
+ return ResolvedValue.ofValue(constantValue());
+ }
+ Value value = valueFinder.apply(index, identifier == null ? "" : identifier);
+ if (value == null) {
+ return ResolvedValue.missing();
+ }
+ if (structIdentifiers.isEmpty()) {
+ return ResolvedValue.ofValue(value);
+ }
+ Value current = value;
+ // structIdentifiers is a path of list indices into nested STRUCT values.
+ // STRUCT values are represented as ListValue in field order.
+ for (int structIndex : structIdentifiers) {
+ if (current.getKindCase() != Value.KindCase.LIST_VALUE
+ || structIndex < 0
+ || structIndex >= current.getListValue().getValuesCount()) {
+ return ResolvedValue.failed();
+ }
+ current = current.getListValue().getValues(structIndex);
+ }
+ return ResolvedValue.ofValue(current);
+ }
+
+ private boolean shouldConsumeValueIndex() {
+ return !hasConstantValue() && !random;
+ }
+
+ static Part fromProto(com.google.spanner.v1.KeyRecipe.Part partProto) {
+ if (partProto.getTag() != 0) {
+ if (partProto.getTag() < 0) {
+ return new Part(Kind.INVALID, 0, null, null, null, null, null, null, false);
+ }
+ return new Part(Kind.TAG, partProto.getTag(), null, null, null, null, null, null, false);
+ }
+ if (!partProto.hasType()) {
+ return new Part(Kind.INVALID, 0, null, null, null, null, null, null, false);
+ }
+ if (partProto.getOrder() != com.google.spanner.v1.KeyRecipe.Part.Order.ASCENDING
+ && partProto.getOrder() != com.google.spanner.v1.KeyRecipe.Part.Order.DESCENDING) {
+ return new Part(Kind.INVALID, 0, null, null, null, null, null, null, false);
+ }
+ if (partProto.getNullOrder() != com.google.spanner.v1.KeyRecipe.Part.NullOrder.NULLS_FIRST
+ && partProto.getNullOrder() != com.google.spanner.v1.KeyRecipe.Part.NullOrder.NULLS_LAST
+ && partProto.getNullOrder() != com.google.spanner.v1.KeyRecipe.Part.NullOrder.NOT_NULL) {
+ return new Part(Kind.INVALID, 0, null, null, null, null, null, null, false);
+ }
+ if (partProto.hasRandom()
+ && partProto.getType().getCode() != com.google.spanner.v1.TypeCode.INT64) {
+ return new Part(Kind.INVALID, 0, null, null, null, null, null, null, false);
+ }
+
+ String identifier = partProto.hasIdentifier() ? partProto.getIdentifier() : null;
+ List structIdentifiers = new ArrayList<>(partProto.getStructIdentifiersList());
+
+ Value constantValue = partProto.hasValue() ? partProto.getValue() : null;
+
+ return new Part(
+ Kind.VALUE,
+ 0,
+ partProto.getType(),
+ partProto.getOrder(),
+ partProto.getNullOrder(),
+ identifier,
+ structIdentifiers,
+ constantValue,
+ partProto.hasRandom());
+ }
+ }
+
+ private static void encodeRandomValuePart(Part part, UnsynchronizedByteArrayOutputStream out) {
+ long value = ThreadLocalRandom.current().nextLong(0, Long.MAX_VALUE);
+ boolean ascending = part.order == com.google.spanner.v1.KeyRecipe.Part.Order.ASCENDING;
+ if (ascending) {
+ SsFormat.appendInt64Increasing(out, value);
+ } else {
+ SsFormat.appendInt64Decreasing(out, value);
+ }
+ }
+
+ private static final class ResolvedValue {
+ private final Value value;
+ private final boolean found;
+ private final boolean failed;
+
+ private ResolvedValue(Value value, boolean found, boolean failed) {
+ this.value = value;
+ this.found = found;
+ this.failed = failed;
+ }
+
+ private static ResolvedValue ofValue(Value value) {
+ return new ResolvedValue(value, true, false);
+ }
+
+ private static ResolvedValue missing() {
+ return new ResolvedValue(null, false, false);
+ }
+
+ private static ResolvedValue failed() {
+ return new ResolvedValue(null, false, true);
+ }
+ }
+
+ private final List parts;
+ private final boolean isIndex;
+
+ private KeyRecipe(List parts, boolean isIndex) {
+ this.parts = parts;
+ this.isIndex = isIndex;
+ }
+
+ public static KeyRecipe create(com.google.spanner.v1.KeyRecipe in) {
+ if (in.getPartCount() == 0) {
+ throw new IllegalArgumentException("KeyRecipe must have at least one part.");
+ }
+ boolean isIndex = in.hasIndexName();
+ List partsList =
+ in.getPartList().stream().map(Part::fromProto).collect(Collectors.toList());
+ if (partsList.get(0).kind != Kind.TAG) {
+ throw new IllegalArgumentException("KeyRecipe must start with a tag.");
+ }
+ return new KeyRecipe(partsList, isIndex);
+ }
+
+ private static void encodeNull(Part part, UnsynchronizedByteArrayOutputStream out) {
+ switch (part.nullOrder) {
+ case NULLS_FIRST:
+ SsFormat.appendNullOrderedFirst(out);
+ break;
+ case NULLS_LAST:
+ SsFormat.appendNullOrderedLast(out);
+ break;
+ case NOT_NULL:
+ throw new IllegalArgumentException("Key part cannot be NULL");
+ default:
+ throw new IllegalArgumentException("Unknown null order: " + part.nullOrder);
+ }
+ }
+
+ private static void encodeNotNull(Part part, UnsynchronizedByteArrayOutputStream out) {
+ switch (part.nullOrder) {
+ case NULLS_FIRST:
+ SsFormat.appendNotNullMarkerNullOrderedFirst(out);
+ break;
+ case NULLS_LAST:
+ SsFormat.appendNotNullMarkerNullOrderedLast(out);
+ break;
+ case NOT_NULL:
+ // No marker needed for NOT_NULL
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown null order: " + part.nullOrder);
+ }
+ }
+
+ private static void encodeSingleValuePart(
+ Part part, Value value, UnsynchronizedByteArrayOutputStream out) {
+ if (value.getKindCase() == Value.KindCase.NULL_VALUE) {
+ encodeNull(part, out);
+ return;
+ }
+
+ // Validate type compatibility BEFORE encoding anything
+ validateValueType(part, value);
+
+ // Now safe to encode the NOT_NULL marker
+ encodeNotNull(part, out);
+
+ boolean isAscending = (part.order == com.google.spanner.v1.KeyRecipe.Part.Order.ASCENDING);
+
+ switch (part.type.getCode()) {
+ case BOOL:
+ if (isAscending) {
+ SsFormat.appendBoolIncreasing(out, value.getBoolValue());
+ } else {
+ SsFormat.appendBoolDecreasing(out, value.getBoolValue());
+ }
+ break;
+ case INT64:
+ long intVal = Long.parseLong(value.getStringValue());
+ if (isAscending) {
+ SsFormat.appendInt64Increasing(out, intVal);
+ } else {
+ SsFormat.appendInt64Decreasing(out, intVal);
+ }
+ break;
+ case FLOAT64:
+ double dblVal;
+ if (value.getKindCase() == Value.KindCase.STRING_VALUE) {
+ // Handle special float values like Infinity, -Infinity, NaN
+ String strVal = value.getStringValue();
+ if ("Infinity".equals(strVal)) {
+ dblVal = Double.POSITIVE_INFINITY;
+ } else if ("-Infinity".equals(strVal)) {
+ dblVal = Double.NEGATIVE_INFINITY;
+ } else if ("NaN".equals(strVal)) {
+ dblVal = Double.NaN;
+ } else {
+ throw new IllegalArgumentException("Invalid FLOAT64 string: " + strVal);
+ }
+ } else {
+ dblVal = value.getNumberValue();
+ }
+ if (isAscending) {
+ SsFormat.appendDoubleIncreasing(out, dblVal);
+ } else {
+ SsFormat.appendDoubleDecreasing(out, dblVal);
+ }
+ break;
+ case STRING:
+ if (isAscending) {
+ SsFormat.appendStringIncreasing(out, value.getStringValue());
+ } else {
+ SsFormat.appendStringDecreasing(out, value.getStringValue());
+ }
+ break;
+ case BYTES:
+ byte[] bytesDecoded = Base64.getDecoder().decode(value.getStringValue());
+ if (isAscending) {
+ SsFormat.appendBytesIncreasing(out, bytesDecoded);
+ } else {
+ SsFormat.appendBytesDecreasing(out, bytesDecoded);
+ }
+ break;
+ case TIMESTAMP:
+ String tsStr = value.getStringValue();
+ long[] parsed = parseTimestamp(tsStr);
+ byte[] encoded = SsFormat.encodeTimestamp(parsed[0], (int) parsed[1]);
+ if (isAscending) {
+ SsFormat.appendBytesIncreasing(out, encoded);
+ } else {
+ SsFormat.appendBytesDecreasing(out, encoded);
+ }
+ break;
+ case DATE:
+ String dateStr = value.getStringValue();
+ int daysSinceEpoch = parseDate(dateStr);
+ if (isAscending) {
+ SsFormat.appendInt64Increasing(out, daysSinceEpoch);
+ } else {
+ SsFormat.appendInt64Decreasing(out, daysSinceEpoch);
+ }
+ break;
+ case UUID:
+ String uuidStr = value.getStringValue();
+ long[] parsedUuid = parseUuid(uuidStr);
+ byte[] encodedUuid = SsFormat.encodeUuid(parsedUuid[0], parsedUuid[1]);
+ if (isAscending) {
+ SsFormat.appendBytesIncreasing(out, encodedUuid);
+ } else {
+ SsFormat.appendBytesDecreasing(out, encodedUuid);
+ }
+ break;
+ case ENUM:
+ // ENUM values are sent as string representation of the enum number
+ long enumVal = Long.parseLong(value.getStringValue());
+ if (isAscending) {
+ SsFormat.appendInt64Increasing(out, enumVal);
+ } else {
+ SsFormat.appendInt64Decreasing(out, enumVal);
+ }
+ break;
+ case NUMERIC:
+ case TYPE_CODE_UNSPECIFIED:
+ case ARRAY:
+ case STRUCT:
+ case PROTO:
+ case UNRECOGNIZED:
+ default:
+ throw new IllegalArgumentException(
+ "Unsupported type code for ssformat encoding: " + part.type.getCode());
+ }
+ }
+
+ private static void validateValueType(Part part, Value value) {
+ switch (part.type.getCode()) {
+ case BOOL:
+ if (value.getKindCase() != Value.KindCase.BOOL_VALUE) {
+ throw new IllegalArgumentException("Type mismatch for BOOL.");
+ }
+ break;
+ case INT64:
+ if (value.getKindCase() != Value.KindCase.STRING_VALUE) {
+ throw new IllegalArgumentException("Type mismatch for INT64, expecting decimal string.");
+ }
+ // Also validate it's a valid integer
+ try {
+ Long.parseLong(value.getStringValue());
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Invalid INT64 string: " + value.getStringValue(), e);
+ }
+ break;
+ case FLOAT64:
+ if (value.getKindCase() != Value.KindCase.NUMBER_VALUE
+ && value.getKindCase() != Value.KindCase.STRING_VALUE) {
+ throw new IllegalArgumentException("Type mismatch for FLOAT64.");
+ }
+ if (value.getKindCase() == Value.KindCase.STRING_VALUE) {
+ String strVal = value.getStringValue();
+ if (!"Infinity".equals(strVal) && !"-Infinity".equals(strVal) && !"NaN".equals(strVal)) {
+ throw new IllegalArgumentException("Invalid FLOAT64 string: " + strVal);
+ }
+ }
+ break;
+ case STRING:
+ if (value.getKindCase() != Value.KindCase.STRING_VALUE) {
+ throw new IllegalArgumentException("Type mismatch for STRING.");
+ }
+ break;
+ case BYTES:
+ if (value.getKindCase() != Value.KindCase.STRING_VALUE) {
+ throw new IllegalArgumentException("Type mismatch for BYTES, expecting base64 string.");
+ }
+ // Validate base64
+ try {
+ Base64.getDecoder().decode(value.getStringValue());
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Invalid base64 for BYTES type.", e);
+ }
+ break;
+ case TIMESTAMP:
+ if (value.getKindCase() != Value.KindCase.STRING_VALUE) {
+ throw new IllegalArgumentException("Type mismatch for TIMESTAMP.");
+ }
+ // Validate timestamp format: must end with Z (UTC) and be RFC3339
+ validateTimestamp(value.getStringValue());
+ break;
+ case DATE:
+ if (value.getKindCase() != Value.KindCase.STRING_VALUE) {
+ throw new IllegalArgumentException("Type mismatch for DATE.");
+ }
+ // Validate date format: YYYY-MM-DD, exactly 10 chars
+ validateDate(value.getStringValue());
+ break;
+ case UUID:
+ if (value.getKindCase() != Value.KindCase.STRING_VALUE) {
+ throw new IllegalArgumentException("Type mismatch for UUID.");
+ }
+ // Validate UUID format
+ validateUuid(value.getStringValue());
+ break;
+ case ENUM:
+ if (value.getKindCase() != Value.KindCase.STRING_VALUE) {
+ throw new IllegalArgumentException("Type mismatch for ENUM, expecting string.");
+ }
+ // Validate it's a valid integer string
+ try {
+ Long.parseLong(value.getStringValue());
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(
+ "Invalid ENUM string (expecting number): " + value.getStringValue(), e);
+ }
+ break;
+ case NUMERIC:
+ case TYPE_CODE_UNSPECIFIED:
+ case ARRAY:
+ case STRUCT:
+ case PROTO:
+ case UNRECOGNIZED:
+ default:
+ throw new IllegalArgumentException(
+ "Unsupported type code for ssformat encoding: " + part.type.getCode());
+ }
+ }
+
+ private static void validateTimestamp(String ts) {
+ parseTimestamp(ts);
+ }
+
+ private static long[] parseTimestamp(String ts) {
+ if (!ts.endsWith("Z")) {
+ throw new IllegalArgumentException("Invalid TIMESTAMP string: " + ts);
+ }
+ String withoutZ = ts.substring(0, ts.length() - 1);
+ int tIndex = withoutZ.indexOf('T');
+ if (tIndex <= 0 || tIndex == withoutZ.length() - 1) {
+ throw new IllegalArgumentException("Invalid TIMESTAMP string: " + ts);
+ }
+
+ String datePart = withoutZ.substring(0, tIndex);
+ String timePart = withoutZ.substring(tIndex + 1);
+ LocalDate date;
+ try {
+ date = LocalDate.parse(datePart, DATE_FORMATTER);
+ } catch (DateTimeParseException e) {
+ throw new IllegalArgumentException("Invalid TIMESTAMP string: " + ts, e);
+ }
+
+ int nanos = 0;
+ String timeMain = timePart;
+ int dotIndex = timePart.indexOf('.');
+ if (dotIndex >= 0) {
+ timeMain = timePart.substring(0, dotIndex);
+ String fracStr = timePart.substring(dotIndex + 1);
+ if (fracStr.isEmpty()) {
+ throw new IllegalArgumentException("Invalid TIMESTAMP string: " + ts);
+ }
+ for (int i = 0; i < fracStr.length(); i++) {
+ char c = fracStr.charAt(i);
+ if (c < '0' || c > '9') {
+ throw new IllegalArgumentException("Invalid TIMESTAMP string: " + ts);
+ }
+ }
+ while (fracStr.length() < 9) {
+ fracStr = fracStr + "0";
+ }
+ if (fracStr.length() > 9) {
+ fracStr = fracStr.substring(0, 9);
+ }
+ nanos = Integer.parseInt(fracStr);
+ }
+
+ String[] timeParts = timeMain.split(":");
+ if (timeParts.length != 3) {
+ throw new IllegalArgumentException("Invalid TIMESTAMP string: " + ts);
+ }
+ int hour;
+ int minute;
+ int second;
+ try {
+ hour = Integer.parseInt(timeParts[0]);
+ minute = Integer.parseInt(timeParts[1]);
+ second = Integer.parseInt(timeParts[2]);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Invalid TIMESTAMP string: " + ts, e);
+ }
+ if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) {
+ throw new IllegalArgumentException("Invalid TIMESTAMP string: " + ts);
+ }
+
+ long seconds = date.toEpochDay() * 86400L + hour * 3600L + minute * 60L + second;
+ return new long[] {seconds, nanos};
+ }
+
+ private static final DateTimeFormatter DATE_FORMATTER =
+ DateTimeFormatter.ofPattern("uuuu-MM-dd").withResolverStyle(ResolverStyle.STRICT);
+
+ private static void validateDate(String dateStr) {
+ parseDate(dateStr);
+ }
+
+ private static int parseDate(String dateStr) {
+ try {
+ LocalDate date = LocalDate.parse(dateStr, DATE_FORMATTER);
+ return (int) date.toEpochDay();
+ } catch (DateTimeParseException e) {
+ throw new IllegalArgumentException("Invalid DATE string: " + dateStr, e);
+ }
+ }
+
+ private static void validateUuid(String uuid) {
+ parseUuid(uuid);
+ // parseUuid throws if invalid
+ }
+
+ private static long[] parseUuid(String uuid) {
+ String originalUuid = uuid;
+
+ // Handle optional braces
+ if (uuid.startsWith("{")) {
+ if (!uuid.endsWith("}")) {
+ throw new IllegalArgumentException("Invalid UUID string: " + originalUuid);
+ }
+ uuid = uuid.substring(1, uuid.length() - 1);
+ }
+
+ // Minimum 36 characters required (standard UUID format: 8-4-4-4-12)
+ if (uuid.length() < 36) {
+ throw new IllegalArgumentException("Invalid UUID string: " + originalUuid);
+ }
+
+ // Check for leading hyphen
+ if (uuid.startsWith("-")) {
+ throw new IllegalArgumentException("Invalid UUID string: " + originalUuid);
+ }
+
+ // Parse 32 hex digits (ignoring hyphens in between)
+ long high = 0;
+ long low = 0;
+ int hexCount = 0;
+
+ for (int i = 0; i < uuid.length(); i++) {
+ char c = uuid.charAt(i);
+ if (c == '-') {
+ continue; // Skip hyphens
+ }
+ int digit = hexDigit(c);
+ if (digit < 0) {
+ throw new IllegalArgumentException("Invalid UUID string: " + originalUuid);
+ }
+ if (hexCount < 16) {
+ high = (high << 4) | digit;
+ } else {
+ low = (low << 4) | digit;
+ }
+ hexCount++;
+ }
+
+ if (hexCount != 32) {
+ throw new IllegalArgumentException("Invalid UUID string: " + originalUuid);
+ }
+
+ // After parsing, verify there are no trailing characters
+ // (uuid must be exactly consumed)
+ if (uuid.length() > 36) {
+ throw new IllegalArgumentException("Invalid UUID string: " + originalUuid);
+ }
+
+ return new long[] {high, low};
+ }
+
+ private static int hexDigit(char c) {
+ if (c >= '0' && c <= '9') return c - '0';
+ if (c >= 'a' && c <= 'f') return 10 + (c - 'a');
+ if (c >= 'A' && c <= 'F') return 10 + (c - 'A');
+ return -1;
+ }
+
+ private TargetRange encodeKeyInternal(
+ BiFunction valueFinder, KeyType keyType) {
+ UnsynchronizedByteArrayOutputStream ssKey = new UnsynchronizedByteArrayOutputStream();
+ int valueIdx = 0;
+ EncodeState state = EncodeState.OK;
+ int p = 0;
+ for (; p < parts.size(); ++p) {
+ final Part part = parts.get(p);
+ if (part.kind == Kind.TAG) {
+ SsFormat.appendCompositeTag(ssKey, part.tag);
+ } else if (part.kind == Kind.VALUE) {
+ if (part.random) {
+ encodeRandomValuePart(part, ssKey);
+ continue;
+ }
+
+ int currentIndex = valueIdx;
+ if (part.shouldConsumeValueIndex()) {
+ valueIdx++;
+ }
+ ResolvedValue resolved = part.resolveValue(valueFinder, currentIndex);
+ if (resolved.failed) {
+ state = EncodeState.FAILED;
+ break;
+ }
+ if (!resolved.found) {
+ state = part.shouldConsumeValueIndex() ? EncodeState.END_OF_KEYS : EncodeState.FAILED;
+ break;
+ }
+ try {
+ encodeSingleValuePart(part, resolved.value, ssKey);
+ } catch (IllegalArgumentException e) {
+ state = EncodeState.FAILED;
+ break;
+ }
+ } else {
+ state = EncodeState.FAILED;
+ break;
+ }
+ }
+
+ ByteString start = ByteString.copyFrom(ssKey.toByteArray());
+ ByteString limit = ByteString.EMPTY;
+ boolean approximate = false;
+
+ if (p == parts.size() || (keyType != KeyType.FULL_KEY && state == EncodeState.END_OF_KEYS)) {
+ if (keyType == KeyType.PREFIX_SUCCESSOR) {
+ start = SsFormat.makePrefixSuccessor(start);
+ } else if (keyType == KeyType.INDEX_KEY) {
+ limit = SsFormat.makePrefixSuccessor(start);
+ }
+ } else {
+ approximate = true;
+ limit = SsFormat.makePrefixSuccessor(start);
+ }
+ return new TargetRange(start, limit, approximate);
+ }
+
+ public TargetRange keyToTargetRange(ListValue in) {
+ return encodeKeyInternal(
+ (index, identifier) -> {
+ if (index < 0 || index >= in.getValuesCount()) {
+ return null;
+ }
+ return in.getValues(index);
+ },
+ isIndex ? KeyType.INDEX_KEY : KeyType.FULL_KEY);
+ }
+
+ public TargetRange keyRangeToTargetRange(KeyRange in) {
+ TargetRange start;
+ switch (in.getStartKeyTypeCase()) {
+ case START_CLOSED:
+ start =
+ encodeKeyInternal(
+ (index, id) -> {
+ if (index < 0 || index >= in.getStartClosed().getValuesCount()) {
+ return null;
+ }
+ return in.getStartClosed().getValues(index);
+ },
+ KeyType.PREFIX);
+ break;
+ case START_OPEN:
+ start =
+ encodeKeyInternal(
+ (index, id) -> {
+ if (index < 0 || index >= in.getStartOpen().getValuesCount()) {
+ return null;
+ }
+ return in.getStartOpen().getValues(index);
+ },
+ KeyType.PREFIX_SUCCESSOR);
+ break;
+ default:
+ start = encodeKeyInternal((index, id) -> null, KeyType.PREFIX);
+ start.approximate = true;
+ break;
+ }
+
+ TargetRange limit;
+ switch (in.getEndKeyTypeCase()) {
+ case END_CLOSED:
+ limit =
+ encodeKeyInternal(
+ (index, id) -> {
+ if (index < 0 || index >= in.getEndClosed().getValuesCount()) {
+ return null;
+ }
+ return in.getEndClosed().getValues(index);
+ },
+ KeyType.PREFIX_SUCCESSOR);
+ break;
+ case END_OPEN:
+ limit =
+ encodeKeyInternal(
+ (index, id) -> {
+ if (index < 0 || index >= in.getEndOpen().getValuesCount()) {
+ return null;
+ }
+ return in.getEndOpen().getValues(index);
+ },
+ KeyType.PREFIX);
+ break;
+ default:
+ limit = encodeKeyInternal((index, id) -> null, KeyType.PREFIX_SUCCESSOR);
+ limit.approximate = true;
+ break;
+ }
+ ByteString limitKey = limit.approximate ? limit.limit : limit.start;
+ return new TargetRange(start.start, limitKey, start.approximate || limit.approximate);
+ }
+
+ public TargetRange keySetToTargetRange(KeySet in) {
+ if (in.getAll()) {
+ return keyRangeToTargetRange(
+ KeyRange.newBuilder()
+ .setStartClosed(ListValue.getDefaultInstance())
+ .setEndClosed(ListValue.getDefaultInstance())
+ .build());
+ }
+ if (in.getRangesCount() == 0) {
+ if (in.getKeysCount() == 0) {
+ return new TargetRange(ByteString.EMPTY, K_INFINITY, true);
+ } else if (in.getKeysCount() == 1) {
+ return keyToTargetRange(in.getKeys(0));
+ }
+ }
+
+ TargetRange target = new TargetRange(K_INFINITY, ByteString.EMPTY, false);
+ for (ListValue key : in.getKeysList()) {
+ target.mergeFrom(keyToTargetRange(key));
+ }
+ for (KeyRange range : in.getRangesList()) {
+ target.mergeFrom(keyRangeToTargetRange(range));
+ }
+ return target;
+ }
+
+ public TargetRange queryParamsToTargetRange(Struct in) {
+ return encodeKeyInternal(
+ (index, identifier) -> {
+ return in.getFieldsMap().get(identifier);
+ },
+ KeyType.FULL_KEY);
+ }
+
+ public TargetRange mutationToTargetRange(Mutation in) {
+ TargetRange target = new TargetRange(K_INFINITY, ByteString.EMPTY, false);
+
+ switch (in.getOperationCase()) {
+ case INSERT:
+ case UPDATE:
+ case INSERT_OR_UPDATE:
+ case REPLACE:
+ final Mutation.Write write = getWrite(in);
+ for (ListValue values : write.getValuesList()) {
+ target.mergeFrom(
+ encodeKeyInternal(
+ (index, id) -> {
+ int colIndex = write.getColumnsList().indexOf(id);
+ if (colIndex == -1 || colIndex >= values.getValuesCount()) {
+ return null;
+ }
+ return values.getValues(colIndex);
+ },
+ KeyType.FULL_KEY));
+ }
+ break;
+ case DELETE:
+ target.mergeFrom(keySetToTargetRange(in.getDelete().getKeySet()));
+ break;
+ case SEND:
+ target.mergeFrom(keyToTargetRange(in.getSend().getKey()));
+ break;
+ case ACK:
+ target.mergeFrom(keyToTargetRange(in.getAck().getKey()));
+ break;
+ default:
+ break;
+ }
+
+ if (target.start.equals(K_INFINITY)) {
+ target = new TargetRange(ByteString.EMPTY, K_INFINITY, true);
+ }
+ return target;
+ }
+
+ private Mutation.Write getWrite(Mutation in) {
+ switch (in.getOperationCase()) {
+ case INSERT:
+ return in.getInsert();
+ case UPDATE:
+ return in.getUpdate();
+ case INSERT_OR_UPDATE:
+ return in.getInsertOrUpdate();
+ case REPLACE:
+ return in.getReplace();
+ default:
+ throw new IllegalArgumentException("Mutation is not a write operation");
+ }
+ }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/KeyRecipeCache.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/KeyRecipeCache.java
new file mode 100644
index 0000000000..7fbedea449
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/KeyRecipeCache.java
@@ -0,0 +1,331 @@
+/*
+ * 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.common.annotations.VisibleForTesting;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.ImmutableList;
+import com.google.common.hash.Hasher;
+import com.google.common.hash.Hashing;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.Value;
+import com.google.spanner.v1.ExecuteSqlRequest;
+import com.google.spanner.v1.ReadRequest;
+import com.google.spanner.v1.RecipeList;
+import com.google.spanner.v1.RoutingHint;
+import com.google.spanner.v1.Type;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.logging.Logger;
+
+@InternalApi
+public final class KeyRecipeCache {
+ private static final Logger logger = Logger.getLogger(KeyRecipeCache.class.getName());
+ private static final long DEFAULT_SCHEMA_RECIPE_CACHE_SIZE = 1000;
+ private static final long DEFAULT_PREPARED_QUERY_CACHE_SIZE = 1000;
+ private static final long DEFAULT_PREPARED_READ_CACHE_SIZE = 1000;
+
+ @VisibleForTesting
+ static long fingerprint(ReadRequest req) {
+ Hasher hasher = Hashing.goodFastHash(64).newHasher();
+ hasher.putString(req.getTable(), StandardCharsets.UTF_8);
+ hasher.putString(req.getIndex(), StandardCharsets.UTF_8);
+ hasher.putInt(req.getColumnsCount());
+ for (String column : req.getColumnsList()) {
+ hasher.putString(column, StandardCharsets.UTF_8);
+ }
+ return hasher.hash().asLong();
+ }
+
+ @VisibleForTesting
+ static long fingerprint(ExecuteSqlRequest req) {
+ Hasher hasher = Hashing.goodFastHash(64).newHasher();
+ hasher.putString(req.getSql(), StandardCharsets.UTF_8);
+
+ List paramNames = new ArrayList<>(req.getParams().getFieldsMap().keySet());
+ paramNames.sort(Comparator.naturalOrder());
+ for (String name : paramNames) {
+ hasher.putString(name, StandardCharsets.UTF_8);
+ if (req.getParamTypesMap().containsKey(name)) {
+ hasher.putBytes(req.getParamTypesMap().get(name).toByteArray());
+ } else {
+ Value value = req.getParams().getFieldsMap().get(name);
+ hasher.putInt(value.getKindCase().getNumber());
+ }
+ }
+
+ hasher.putBytes(req.getQueryOptions().toByteArray());
+ return hasher.hash().asLong();
+ }
+
+ private final AtomicLong nextOperationUid = new AtomicLong(1);
+ private ByteString schemaGeneration = ByteString.EMPTY;
+
+ private final Cache schemaRecipes =
+ CacheBuilder.newBuilder().maximumSize(DEFAULT_SCHEMA_RECIPE_CACHE_SIZE).build();
+ private final Cache queryRecipes =
+ CacheBuilder.newBuilder().maximumSize(DEFAULT_PREPARED_QUERY_CACHE_SIZE).build();
+ private final Cache preparedReads =
+ CacheBuilder.newBuilder().maximumSize(DEFAULT_PREPARED_READ_CACHE_SIZE).build();
+ private final Cache preparedQueries =
+ CacheBuilder.newBuilder().maximumSize(DEFAULT_PREPARED_QUERY_CACHE_SIZE).build();
+
+ public KeyRecipeCache() {}
+
+ private static V getIfPresent(Cache cache, K key) {
+ return cache.getIfPresent(key);
+ }
+
+ @VisibleForTesting
+ static int getPreparedReadCacheSize(KeyRecipeCache cache) {
+ return (int) cache.preparedReads.size();
+ }
+
+ @VisibleForTesting
+ static int getPreparedQueryCacheSize(KeyRecipeCache cache) {
+ return (int) cache.preparedQueries.size();
+ }
+
+ /**
+ * Applies recipes from a server CacheUpdate.
+ *
+ * This is expected to be called only when responses include new recipes, not on every request.
+ * It is synchronized to atomically update schema generation and cache contents.
+ */
+ public synchronized void addRecipes(RecipeList recipeList) {
+ int cmp =
+ ByteString.unsignedLexicographicalComparator()
+ .compare(recipeList.getSchemaGeneration(), schemaGeneration);
+ if (cmp < 0) {
+ return;
+ }
+ if (cmp > 0) {
+ schemaGeneration = recipeList.getSchemaGeneration();
+ schemaRecipes.invalidateAll();
+ queryRecipes.invalidateAll();
+ }
+
+ int failedCount = 0;
+ IllegalArgumentException failureExample = null;
+ for (com.google.spanner.v1.KeyRecipe recipeProto : recipeList.getRecipeList()) {
+ try {
+ KeyRecipe recipe = KeyRecipe.create(recipeProto);
+ if (recipeProto.hasTableName()) {
+ schemaRecipes.put(recipeProto.getTableName(), recipe);
+ } else if (recipeProto.hasIndexName()) {
+ schemaRecipes.put(recipeProto.getIndexName(), recipe);
+ } else if (recipeProto.hasOperationUid()) {
+ queryRecipes.put(recipeProto.getOperationUid(), recipe);
+ }
+ } catch (IllegalArgumentException e) {
+ failedCount++;
+ if (failureExample == null) {
+ failureExample = e;
+ }
+ }
+ }
+ if (failedCount > 0) {
+ logger.warning(
+ "Failed to add " + failedCount + " recipes, example: " + failureExample.getMessage());
+ }
+ }
+
+ public void computeKeys(ReadRequest.Builder reqBuilder) {
+ long reqFp = fingerprint(reqBuilder.buildPartial());
+
+ RoutingHint.Builder hintBuilder = reqBuilder.getRoutingHintBuilder();
+ if (!schemaGeneration.isEmpty()) {
+ hintBuilder.setSchemaGeneration(schemaGeneration);
+ }
+
+ PreparedRead preparedRead = getIfPresent(preparedReads, reqFp);
+ if (preparedRead == null) {
+ preparedRead = PreparedRead.fromRequest(reqBuilder.buildPartial());
+ preparedRead.operationUid = nextOperationUid.getAndIncrement();
+ preparedReads.put(reqFp, preparedRead);
+ } else if (!preparedRead.matches(reqBuilder.buildPartial())) {
+ logger.fine("Fingerprint collision for ReadRequest: " + reqFp);
+ return;
+ }
+
+ hintBuilder.setOperationUid(preparedRead.operationUid);
+ String recipeKey = reqBuilder.getTable();
+ if (!reqBuilder.getIndex().isEmpty()) {
+ recipeKey = reqBuilder.getIndex();
+ }
+
+ KeyRecipe recipe = getIfPresent(schemaRecipes, recipeKey);
+ if (recipe == null) {
+ logger.fine("Schema recipe not found for: " + recipeKey);
+ return;
+ }
+
+ try {
+ TargetRange target = recipe.keySetToTargetRange(reqBuilder.getKeySet());
+ hintBuilder.setKey(target.start);
+ if (!target.limit.isEmpty()) {
+ hintBuilder.setLimitKey(target.limit);
+ }
+ } catch (IllegalArgumentException e) {
+ logger.fine("Failed key encoding: " + e.getMessage());
+ }
+ }
+
+ public void computeKeys(ExecuteSqlRequest.Builder reqBuilder) {
+ long reqFp = fingerprint(reqBuilder.buildPartial());
+
+ RoutingHint.Builder hintBuilder = reqBuilder.getRoutingHintBuilder();
+ if (!schemaGeneration.isEmpty()) {
+ hintBuilder.setSchemaGeneration(schemaGeneration);
+ }
+
+ PreparedQuery preparedQuery = getIfPresent(preparedQueries, reqFp);
+ if (preparedQuery == null) {
+ preparedQuery = PreparedQuery.fromRequest(reqBuilder.buildPartial());
+ preparedQuery.operationUid = nextOperationUid.getAndIncrement();
+ preparedQueries.put(reqFp, preparedQuery);
+ } else if (!preparedQuery.matches(reqBuilder.buildPartial())) {
+ logger.fine("Fingerprint collision for ExecuteSqlRequest: " + reqFp);
+ return;
+ }
+
+ hintBuilder.setOperationUid(preparedQuery.operationUid);
+ KeyRecipe recipe = getIfPresent(queryRecipes, preparedQuery.operationUid);
+ if (recipe == null) {
+ return;
+ }
+
+ try {
+ TargetRange target = recipe.queryParamsToTargetRange(reqBuilder.getParams());
+ hintBuilder.setKey(target.start);
+ if (!target.limit.isEmpty()) {
+ hintBuilder.setLimitKey(target.limit);
+ }
+ } catch (IllegalArgumentException e) {
+ logger.fine("Failed query param encoding: " + e.getMessage());
+ }
+ }
+
+ public synchronized void clear() {
+ schemaGeneration = ByteString.EMPTY;
+ preparedReads.invalidateAll();
+ preparedQueries.invalidateAll();
+ schemaRecipes.invalidateAll();
+ queryRecipes.invalidateAll();
+ }
+
+ private static class PreparedRead {
+ final String table;
+ final ImmutableList columns;
+ long operationUid; // Not final, assigned after construction
+
+ private PreparedRead(String table, List columns) {
+ this.table = table;
+ this.columns = ImmutableList.copyOf(columns);
+ }
+
+ static PreparedRead fromRequest(ReadRequest req) {
+ return new PreparedRead(req.getTable(), req.getColumnsList());
+ }
+
+ boolean matches(ReadRequest req) {
+ if (!Objects.equals(table, req.getTable())) {
+ return false;
+ }
+ return columns.equals(req.getColumnsList());
+ }
+ }
+
+ private static final class PreparedQuery {
+ private final String sql;
+ private final ImmutableList params;
+ private final ExecuteSqlRequest.QueryOptions queryOptions;
+ private long operationUid;
+
+ private PreparedQuery(
+ String sql, List params, ExecuteSqlRequest.QueryOptions queryOptions) {
+ this.sql = sql;
+ this.params = ImmutableList.copyOf(params);
+ this.queryOptions = queryOptions;
+ }
+
+ private static PreparedQuery fromRequest(ExecuteSqlRequest req) {
+ List params = new ArrayList<>();
+ for (Map.Entry entry : req.getParams().getFieldsMap().entrySet()) {
+ String name = entry.getKey();
+ if (req.getParamTypesMap().containsKey(name)) {
+ params.add(Param.ofType(name, req.getParamTypesMap().get(name)));
+ } else {
+ params.add(Param.ofKind(name, entry.getValue().getKindCase()));
+ }
+ }
+ params.sort(Comparator.comparing(param -> param.name));
+ return new PreparedQuery(req.getSql(), params, req.getQueryOptions());
+ }
+
+ private boolean matches(ExecuteSqlRequest req) {
+ if (!sql.equals(req.getSql())) {
+ return false;
+ }
+ if (params.size() != req.getParams().getFieldsCount()) {
+ return false;
+ }
+ for (Param param : params) {
+ Value value = req.getParams().getFieldsMap().get(param.name);
+ if (value == null) {
+ return false;
+ }
+ if (param.type != null) {
+ Type type = req.getParamTypesMap().get(param.name);
+ if (type == null || !type.equals(param.type)) {
+ return false;
+ }
+ } else if (param.kindCase != value.getKindCase()) {
+ return false;
+ }
+ }
+ return Objects.equals(queryOptions, req.getQueryOptions());
+ }
+ }
+
+ private static final class Param {
+ private final String name;
+ private final Type type;
+ private final Value.KindCase kindCase;
+
+ private Param(String name, Type type, Value.KindCase kindCase) {
+ this.name = name;
+ this.type = type;
+ this.kindCase = kindCase;
+ }
+
+ private static Param ofType(String name, Type type) {
+ return new Param(name, type, null);
+ }
+
+ private static Param ofKind(String name, Value.KindCase kindCase) {
+ return new Param(name, null, kindCase);
+ }
+ }
+}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/KeyRecipeCacheTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/KeyRecipeCacheTest.java
new file mode 100644
index 0000000000..bcf89e529a
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/KeyRecipeCacheTest.java
@@ -0,0 +1,201 @@
+/*
+ * 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.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.protobuf.TextFormat;
+import com.google.spanner.v1.ExecuteSqlRequest;
+import com.google.spanner.v1.ReadRequest;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class KeyRecipeCacheTest {
+
+ @Test
+ public void fingerprintReadUsesShape() throws Exception {
+ ReadRequest req =
+ parseRead(
+ "table: \"T\"\n"
+ + "columns: \"c1\"\n"
+ + "columns: \"c2\"\n"
+ + "key_set { keys { values { string_value: \"foo\" } } }\n");
+
+ long fp = KeyRecipeCache.fingerprint(req);
+ assertNotEquals(0, fp);
+ assertEquals(fp, KeyRecipeCache.fingerprint(req));
+
+ ReadRequest diffTable = ReadRequest.newBuilder(req).setTable("U").build();
+ assertNotEquals(fp, KeyRecipeCache.fingerprint(diffTable));
+
+ ReadRequest diffIndex = ReadRequest.newBuilder(req).setIndex("I").build();
+ assertNotEquals(fp, KeyRecipeCache.fingerprint(diffIndex));
+
+ ReadRequest diffColumn = ReadRequest.newBuilder(req).setColumns(0, "c3").build();
+ assertNotEquals(fp, KeyRecipeCache.fingerprint(diffColumn));
+
+ ReadRequest extraColumn = ReadRequest.newBuilder(req).addColumns("c4").build();
+ assertNotEquals(fp, KeyRecipeCache.fingerprint(extraColumn));
+
+ ReadRequest removeColumn = ReadRequest.newBuilder(req).clearColumns().addColumns("c1").build();
+ assertNotEquals(fp, KeyRecipeCache.fingerprint(removeColumn));
+
+ ReadRequest sameShape =
+ ReadRequest.newBuilder(req)
+ .clearKeySet()
+ .setKeySet(req.getKeySet().toBuilder().build())
+ .build();
+ assertEquals(fp, KeyRecipeCache.fingerprint(sameShape));
+
+ ReadRequest.Builder diffKeyValueBuilder = ReadRequest.newBuilder(req);
+ diffKeyValueBuilder
+ .getKeySetBuilder()
+ .getKeysBuilder(0)
+ .getValuesBuilder(0)
+ .setStringValue("bar");
+ ReadRequest diffKeyValue = diffKeyValueBuilder.build();
+ assertEquals(fp, KeyRecipeCache.fingerprint(diffKeyValue));
+ }
+
+ @Test
+ public void fingerprintExecuteSqlUsesParamShape() throws Exception {
+ ExecuteSqlRequest req =
+ parseExecuteSql(
+ "sql: \"SELECT * FROM T WHERE p1 = @p1 AND p2 = @p2\"\n"
+ + "params {\n"
+ + " fields { key: \"p1\" value { string_value: \"foo\" } }\n"
+ + " fields { key: \"p2\" value { string_value: \"99\" } }\n"
+ + "}\n"
+ + "param_types { key: \"p2\" value { code: INT64 } }\n"
+ + "query_options {\n"
+ + " optimizer_version: \"1\"\n"
+ + " optimizer_statistics_package: \"stats\"\n"
+ + "}\n");
+
+ long fp = KeyRecipeCache.fingerprint(req);
+ assertNotEquals(0, fp);
+ assertEquals(fp, KeyRecipeCache.fingerprint(req));
+
+ ExecuteSqlRequest diffSql = ExecuteSqlRequest.newBuilder(req).setSql("SELECT * FROM U").build();
+ assertNotEquals(fp, KeyRecipeCache.fingerprint(diffSql));
+
+ ExecuteSqlRequest.Builder removeParamBuilder = ExecuteSqlRequest.newBuilder(req);
+ removeParamBuilder.getParamsBuilder().removeFields("p1");
+ ExecuteSqlRequest removeParam = removeParamBuilder.build();
+ assertNotEquals(fp, KeyRecipeCache.fingerprint(removeParam));
+
+ ExecuteSqlRequest.Builder addParamBuilder = ExecuteSqlRequest.newBuilder(req);
+ addParamBuilder.getParamsBuilder().putFields("p3", parseValue("string_value: \"foo\""));
+ ExecuteSqlRequest addParam = addParamBuilder.build();
+ assertNotEquals(fp, KeyRecipeCache.fingerprint(addParam));
+
+ ExecuteSqlRequest changeType =
+ ExecuteSqlRequest.newBuilder(req).putParamTypes("p1", parseType("code: BYTES")).build();
+ assertNotEquals(fp, KeyRecipeCache.fingerprint(changeType));
+
+ ExecuteSqlRequest.Builder changeParamValueBuilder = ExecuteSqlRequest.newBuilder(req);
+ changeParamValueBuilder.getParamsBuilder().putFields("p1", parseValue("string_value: \"bar\""));
+ ExecuteSqlRequest changeParamValue = changeParamValueBuilder.build();
+ assertEquals(fp, KeyRecipeCache.fingerprint(changeParamValue));
+
+ ExecuteSqlRequest.Builder changeKindBuilder = ExecuteSqlRequest.newBuilder(req);
+ changeKindBuilder.getParamsBuilder().putFields("p1", parseValue("bool_value: true"));
+ ExecuteSqlRequest changeKind = changeKindBuilder.build();
+ assertNotEquals(fp, KeyRecipeCache.fingerprint(changeKind));
+
+ ExecuteSqlRequest.Builder changeOptionsBuilder = ExecuteSqlRequest.newBuilder(req);
+ changeOptionsBuilder.getQueryOptionsBuilder().setOptimizerStatisticsPackage("stats_v2");
+ ExecuteSqlRequest changeOptions = changeOptionsBuilder.build();
+ assertNotEquals(fp, KeyRecipeCache.fingerprint(changeOptions));
+
+ ExecuteSqlRequest.Builder changeOptimizerBuilder = ExecuteSqlRequest.newBuilder(req);
+ changeOptimizerBuilder.getQueryOptionsBuilder().setOptimizerVersion("2");
+ ExecuteSqlRequest changeOptimizer = changeOptimizerBuilder.build();
+ assertNotEquals(fp, KeyRecipeCache.fingerprint(changeOptimizer));
+
+ ExecuteSqlRequest clearOptions = ExecuteSqlRequest.newBuilder(req).clearQueryOptions().build();
+ assertNotEquals(fp, KeyRecipeCache.fingerprint(clearOptions));
+ }
+
+ @Test
+ public void computeKeysSetsRoutingHint() throws Exception {
+ KeyRecipeCache cache = new KeyRecipeCache();
+ cache.addRecipes(
+ parseRecipeList(
+ "schema_generation: \"1\"\n"
+ + "recipe {\n"
+ + " table_name: \"T\"\n"
+ + " part { tag: 1 }\n"
+ + " part {\n"
+ + " order: ASCENDING\n"
+ + " null_order: NULLS_FIRST\n"
+ + " type { code: STRING }\n"
+ + " identifier: \"k\"\n"
+ + " }\n"
+ + "}\n"));
+
+ ReadRequest.Builder request =
+ parseRead(
+ "table: \"T\"\n"
+ + "columns: \"c1\"\n"
+ + "key_set { keys { values { string_value: \"foo\" } } }\n")
+ .toBuilder();
+
+ cache.computeKeys(request);
+ assertTrue(request.getRoutingHint().getOperationUid() > 0);
+ assertEquals("1", request.getRoutingHint().getSchemaGeneration().toStringUtf8());
+ assertTrue(request.getRoutingHint().getKey().size() > 0);
+ }
+
+ private static ReadRequest parseRead(String text) throws TextFormat.ParseException {
+ ReadRequest.Builder builder = ReadRequest.newBuilder();
+ TextFormat.merge(text, builder);
+ return builder.build();
+ }
+
+ private static ExecuteSqlRequest parseExecuteSql(String text) throws TextFormat.ParseException {
+ ExecuteSqlRequest.Builder builder = ExecuteSqlRequest.newBuilder();
+ TextFormat.merge(text, builder);
+ return builder.build();
+ }
+
+ private static com.google.protobuf.Value parseValue(String text)
+ throws TextFormat.ParseException {
+ com.google.protobuf.Value.Builder builder = com.google.protobuf.Value.newBuilder();
+ TextFormat.merge(text, builder);
+ return builder.build();
+ }
+
+ private static com.google.spanner.v1.Type parseType(String text)
+ throws TextFormat.ParseException {
+ com.google.spanner.v1.Type.Builder builder = com.google.spanner.v1.Type.newBuilder();
+ TextFormat.merge(text, builder);
+ return builder.build();
+ }
+
+ private static com.google.spanner.v1.RecipeList parseRecipeList(String text)
+ throws TextFormat.ParseException {
+ com.google.spanner.v1.RecipeList.Builder builder =
+ com.google.spanner.v1.RecipeList.newBuilder();
+ TextFormat.merge(text, builder);
+ return builder.build();
+ }
+}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/KeyRecipeTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/KeyRecipeTest.java
new file mode 100644
index 0000000000..9f6387c5c4
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/KeyRecipeTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.protobuf.ByteString;
+import com.google.protobuf.Struct;
+import com.google.protobuf.TextFormat;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class KeyRecipeTest {
+
+ @Test
+ public void queryParamsUsesStructIdentifiers() throws Exception {
+ com.google.spanner.v1.KeyRecipe recipeProto =
+ createRecipe(
+ "part { tag: 1 }\n"
+ + "part {\n"
+ + " order: ASCENDING\n"
+ + " null_order: NULLS_FIRST\n"
+ + " type { code: STRING }\n"
+ + " identifier: \"p0\"\n"
+ + " struct_identifiers: 1\n"
+ + "}\n");
+
+ Struct params =
+ parseStruct(
+ "fields {\n"
+ + " key: \"p0\"\n"
+ + " value {\n"
+ + " list_value { values { string_value: \"a\" } values { string_value: \"b\" }"
+ + " }\n"
+ + " }\n"
+ + "}\n");
+
+ KeyRecipe recipe = KeyRecipe.create(recipeProto);
+ TargetRange target = recipe.queryParamsToTargetRange(params);
+ assertEquals(expectedKey("b"), target.start);
+ assertTrue(target.limit.isEmpty());
+ }
+
+ @Test
+ public void queryParamsUsesConstantValue() throws Exception {
+ com.google.spanner.v1.KeyRecipe recipeProto =
+ createRecipe(
+ "part { tag: 1 }\n"
+ + "part {\n"
+ + " order: ASCENDING\n"
+ + " null_order: NULLS_FIRST\n"
+ + " type { code: STRING }\n"
+ + " value { string_value: \"const\" }\n"
+ + "}\n");
+
+ KeyRecipe recipe = KeyRecipe.create(recipeProto);
+ TargetRange target = recipe.queryParamsToTargetRange(Struct.getDefaultInstance());
+ assertEquals(expectedKey("const"), target.start);
+ assertTrue(target.limit.isEmpty());
+ }
+
+ private static com.google.spanner.v1.KeyRecipe createRecipe(String text)
+ throws TextFormat.ParseException {
+ com.google.spanner.v1.KeyRecipe.Builder builder = com.google.spanner.v1.KeyRecipe.newBuilder();
+ TextFormat.merge(text, builder);
+ return builder.build();
+ }
+
+ private static Struct parseStruct(String text) throws TextFormat.ParseException {
+ Struct.Builder builder = Struct.newBuilder();
+ TextFormat.merge(text, builder);
+ return builder.build();
+ }
+
+ private static ByteString expectedKey(String value) {
+ UnsynchronizedByteArrayOutputStream out = new UnsynchronizedByteArrayOutputStream();
+ SsFormat.appendCompositeTag(out, 1);
+ SsFormat.appendNotNullMarkerNullOrderedFirst(out);
+ SsFormat.appendStringIncreasing(out, value);
+ return ByteString.copyFrom(out.toByteArray());
+ }
+}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/RecipeGoldenTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/RecipeGoldenTest.java
new file mode 100644
index 0000000000..e9f243800f
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/RecipeGoldenTest.java
@@ -0,0 +1,405 @@
+/*
+ * 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.assertEquals;
+
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ListValue;
+import com.google.protobuf.Struct;
+import com.google.protobuf.TextFormat;
+import com.google.spanner.v1.KeyRange;
+import com.google.spanner.v1.KeySet;
+import com.google.spanner.v1.Mutation;
+import com.google.spanner.v1.RecipeList;
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class RecipeGoldenTest {
+
+ @Test
+ public void goldenTest() throws Exception {
+ String content;
+ try (InputStream inputStream =
+ getClass().getClassLoader().getResourceAsStream("recipe_test.textproto")) {
+ content =
+ new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
+ .lines()
+ .reduce("", (a, b) -> a + "\n" + b);
+ }
+
+ List testCases = parseTestCases(content);
+
+ for (TestCase testCase : testCases) {
+ System.out.println("Running test case: " + testCase.name);
+
+ // Skip test cases with invalid recipes that couldn't be parsed
+ if (testCase.invalidRecipe) {
+ for (TestInstance test : testCase.tests) {
+ assertEquals(
+ "Invalid recipe should result in approximate=true in test case: " + testCase.name,
+ true,
+ test.expectedApproximate);
+ }
+ continue;
+ }
+
+ // Skip random tests due to PRNG differences
+ if (testCase.name.contains("Random")) {
+ continue;
+ }
+
+ KeyRecipe recipe;
+ try {
+ recipe = KeyRecipe.create(testCase.recipes.getRecipe(0));
+ } catch (IllegalArgumentException e) {
+ // Invalid recipe - verify all tests expect approximate: true
+ for (TestInstance test : testCase.tests) {
+ assertEquals(
+ "Invalid recipe should result in approximate=true in test case: " + testCase.name,
+ true,
+ test.expectedApproximate);
+ }
+ continue;
+ }
+
+ int testNum = 0;
+ for (TestInstance test : testCase.tests) {
+ testNum++;
+
+ TargetRange target = null;
+ switch (test.operationType) {
+ case "key":
+ target = recipe.keyToTargetRange(test.key);
+ break;
+ case "key_range":
+ target = recipe.keyRangeToTargetRange(test.keyRange);
+ break;
+ case "key_set":
+ target = recipe.keySetToTargetRange(test.keySet);
+ break;
+ case "mutation":
+ target = recipe.mutationToTargetRange(test.mutation);
+ break;
+ case "query_params":
+ target = recipe.queryParamsToTargetRange(test.queryParams);
+ break;
+ default:
+ throw new UnsupportedOperationException("Unsupported operation: " + test.operationType);
+ }
+
+ assertEquals(
+ "Start mismatch in test case: " + testCase.name + " test #" + testNum,
+ test.expectedStart,
+ target.start);
+ assertEquals(
+ "Limit mismatch in test case: " + testCase.name + " test #" + testNum,
+ test.expectedLimit,
+ target.limit);
+ assertEquals(
+ "Approximate mismatch in test case: " + testCase.name + " test #" + testNum,
+ test.expectedApproximate,
+ target.approximate);
+ }
+ }
+ }
+
+ private static class TestCase {
+ String name;
+ RecipeList recipes;
+ List tests = new ArrayList<>();
+ boolean invalidRecipe = false;
+ }
+
+ private static class TestInstance {
+ String operationType;
+ ListValue key;
+ KeyRange keyRange;
+ KeySet keySet;
+ Mutation mutation;
+ Struct queryParams;
+ ByteString expectedStart = ByteString.EMPTY;
+ ByteString expectedLimit = ByteString.EMPTY;
+ boolean expectedApproximate = false;
+ }
+
+ private List parseTestCases(String content) throws Exception {
+ List testCases = new ArrayList<>();
+ int pos = 0;
+
+ while (pos < content.length()) {
+ int testCaseStart = content.indexOf("test_case {", pos);
+ if (testCaseStart == -1) break;
+
+ int testCaseEnd = findMatchingBrace(content, testCaseStart + 10);
+ String testCaseContent = content.substring(testCaseStart + 11, testCaseEnd);
+
+ TestCase tc = parseTestCase(testCaseContent);
+ testCases.add(tc);
+
+ pos = testCaseEnd + 1;
+ }
+
+ return testCases;
+ }
+
+ private TestCase parseTestCase(String content) throws Exception {
+ TestCase tc = new TestCase();
+
+ // Parse name
+ Pattern namePattern = Pattern.compile("name:\\s*\"([^\"]+)\"");
+ Matcher nameMatcher = namePattern.matcher(content);
+ if (nameMatcher.find()) {
+ tc.name = nameMatcher.group(1);
+ }
+
+ // Parse recipes
+ int recipesStart = content.indexOf("recipes {");
+ if (recipesStart != -1) {
+ int recipesEnd = findMatchingBrace(content, recipesStart + 8);
+ String recipesContent = content.substring(recipesStart + 9, recipesEnd);
+ RecipeList.Builder recipesBuilder = RecipeList.newBuilder();
+ try {
+ TextFormat.merge(recipesContent, recipesBuilder);
+ tc.recipes = recipesBuilder.build();
+ } catch (TextFormat.ParseException e) {
+ // Invalid recipe - skip this test case but mark it as having invalid recipes
+ tc.invalidRecipe = true;
+ System.out.println("Skipping test case with invalid recipe: " + tc.name);
+ }
+ }
+
+ // Parse tests
+ int pos = 0;
+ while (pos < content.length()) {
+ // Find "test {" that's not part of "test_case"
+ int testStart = findNextTest(content, pos);
+ if (testStart == -1) break;
+
+ // "test {" is 6 chars, { is at position testStart + 5
+ int bracePos = testStart + 5;
+ int testEnd = findMatchingBrace(content, bracePos);
+ String testContent = content.substring(bracePos + 1, testEnd);
+
+ TestInstance test = parseTest(testContent);
+ tc.tests.add(test);
+
+ pos = testEnd + 1;
+ }
+
+ return tc;
+ }
+
+ private int findNextTest(String content, int start) {
+ int pos = start;
+ while (true) {
+ int testPos = content.indexOf("test {", pos);
+ if (testPos == -1) return -1;
+
+ // Make sure this is not part of "test_case {"
+ if (testPos >= 5) {
+ String before = content.substring(testPos - 5, testPos);
+ if (before.contains("_")) {
+ pos = testPos + 1;
+ continue;
+ }
+ }
+ return testPos;
+ }
+ }
+
+ private TestInstance parseTest(String content) throws Exception {
+ TestInstance test = new TestInstance();
+
+ // Determine operation type and parse operation
+ // NOTE: Check mutation FIRST since it can contain nested key_set/key_range/key
+ if (content.contains("mutation {")) {
+ test.operationType = "mutation";
+ int start = content.indexOf("mutation {");
+ int end = findMatchingBrace(content, start + 9);
+ String mutationContent = content.substring(start + 10, end);
+ Mutation.Builder builder = Mutation.newBuilder();
+ TextFormat.merge(mutationContent, builder);
+ test.mutation = builder.build();
+ } else if (content.contains("query_params {")) {
+ test.operationType = "query_params";
+ int start = content.indexOf("query_params {");
+ int end = findMatchingBrace(content, start + 13);
+ String queryParamsContent = content.substring(start + 14, end);
+ Struct.Builder builder = Struct.newBuilder();
+ TextFormat.merge(queryParamsContent, builder);
+ test.queryParams = builder.build();
+ } else if (content.contains("key_set {")) {
+ test.operationType = "key_set";
+ int start = content.indexOf("key_set {");
+ int end = findMatchingBrace(content, start + 8);
+ String keySetContent = content.substring(start + 9, end);
+ KeySet.Builder builder = KeySet.newBuilder();
+ TextFormat.merge(keySetContent, builder);
+ test.keySet = builder.build();
+ } else if (content.contains("key_range {")) {
+ test.operationType = "key_range";
+ int start = content.indexOf("key_range {");
+ int end = findMatchingBrace(content, start + 10);
+ String keyRangeContent = content.substring(start + 11, end);
+ KeyRange.Builder builder = KeyRange.newBuilder();
+ TextFormat.merge(keyRangeContent, builder);
+ test.keyRange = builder.build();
+ } else if (content.contains("key {")
+ && !content.contains("key_range")
+ && !content.contains("key_set")
+ && !content.contains("limit_key")) {
+ test.operationType = "key";
+ int keyStart = content.indexOf("key {");
+ int keyEnd = findMatchingBrace(content, keyStart + 4);
+ String keyContent = content.substring(keyStart + 5, keyEnd);
+ ListValue.Builder keyBuilder = ListValue.newBuilder();
+ TextFormat.merge(keyContent, keyBuilder);
+ test.key = keyBuilder.build();
+ }
+
+ // Parse expected start
+ Pattern startPattern = Pattern.compile("start:\\s*\"([^\"]*)\"");
+ Matcher startMatcher = startPattern.matcher(content);
+ if (startMatcher.find()) {
+ test.expectedStart = parseEscapedString(startMatcher.group(1));
+ }
+
+ // Parse expected limit
+ Pattern limitPattern = Pattern.compile("(? 0) {
+ char c = content.charAt(pos);
+
+ if (escape) {
+ escape = false;
+ pos++;
+ continue;
+ }
+
+ if (c == '\\') {
+ escape = true;
+ pos++;
+ continue;
+ }
+
+ if (c == '"') {
+ inString = !inString;
+ } else if (!inString) {
+ if (c == '{') {
+ depth++;
+ } else if (c == '}') {
+ depth--;
+ }
+ }
+ pos++;
+ }
+ return pos - 1;
+ }
+
+ private static String bytesToHex(ByteString bs) {
+ StringBuilder sb = new StringBuilder();
+ for (byte b : bs.toByteArray()) {
+ sb.append(String.format("%02x ", b & 0xFF));
+ }
+ return sb.toString();
+ }
+
+ private ByteString parseEscapedString(String escaped) {
+ byte[] bytes = new byte[escaped.length()];
+ int byteIndex = 0;
+ int i = 0;
+
+ while (i < escaped.length()) {
+ char c = escaped.charAt(i);
+ if (c == '\\' && i + 1 < escaped.length()) {
+ char next = escaped.charAt(i + 1);
+ if (next >= '0' && next <= '7') {
+ // Octal escape
+ int value = 0;
+ int count = 0;
+ while (i + 1 < escaped.length()
+ && count < 3
+ && escaped.charAt(i + 1) >= '0'
+ && escaped.charAt(i + 1) <= '7') {
+ value = value * 8 + (escaped.charAt(i + 1) - '0');
+ i++;
+ count++;
+ }
+ bytes[byteIndex++] = (byte) value;
+ } else if (next == 'n') {
+ bytes[byteIndex++] = '\n';
+ i++;
+ } else if (next == 't') {
+ bytes[byteIndex++] = '\t';
+ i++;
+ } else if (next == 'r') {
+ bytes[byteIndex++] = '\r';
+ i++;
+ } else if (next == '\\') {
+ bytes[byteIndex++] = '\\';
+ i++;
+ } else if (next == '"') {
+ bytes[byteIndex++] = '"';
+ i++;
+ } else if (next == 'x' && i + 3 < escaped.length()) {
+ // Hex escape \xNN
+ int value = Integer.parseInt(escaped.substring(i + 2, i + 4), 16);
+ bytes[byteIndex++] = (byte) value;
+ i += 3;
+ } else {
+ bytes[byteIndex++] = (byte) c;
+ }
+ } else {
+ bytes[byteIndex++] = (byte) c;
+ }
+ i++;
+ }
+
+ return ByteString.copyFrom(bytes, 0, byteIndex);
+ }
+}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/RecipeTestCases.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/RecipeTestCases.java
new file mode 100644
index 0000000000..f50f1c4222
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/RecipeTestCases.java
@@ -0,0 +1,247 @@
+/*
+ * 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 com.google.protobuf.ListValue;
+import com.google.protobuf.Struct;
+import com.google.spanner.v1.KeyRange;
+import com.google.spanner.v1.KeySet;
+import com.google.spanner.v1.Mutation;
+import com.google.spanner.v1.RecipeList;
+import java.util.ArrayList;
+import java.util.List;
+
+public final class RecipeTestCases {
+
+ private final List testCases;
+
+ private RecipeTestCases(Builder builder) {
+ this.testCases = new ArrayList<>(builder.testCases);
+ }
+
+ public List getTestCaseList() {
+ return testCases;
+ }
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ public static final class Builder {
+ private final List testCases = new ArrayList<>();
+
+ public Builder addTestCase(RecipeTestCase testCase) {
+ this.testCases.add(testCase);
+ return this;
+ }
+
+ public RecipeTestCases build() {
+ return new RecipeTestCases(this);
+ }
+ }
+
+ public static final class RecipeTestCase {
+ private final String name;
+ private final RecipeList recipes;
+ private final List tests;
+
+ private RecipeTestCase(Builder builder) {
+ this.name = builder.name;
+ this.recipes = builder.recipes;
+ this.tests = new ArrayList<>(builder.tests);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public RecipeList getRecipes() {
+ return recipes;
+ }
+
+ public List getTestList() {
+ return tests;
+ }
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ public static final class Builder {
+ private String name;
+ private RecipeList recipes;
+ private final List tests = new ArrayList<>();
+
+ public Builder setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder setRecipes(RecipeList recipes) {
+ this.recipes = recipes;
+ return this;
+ }
+
+ public Builder addTest(Test test) {
+ this.tests.add(test);
+ return this;
+ }
+
+ public RecipeTestCase build() {
+ return new RecipeTestCase(this);
+ }
+ }
+
+ public static final class Test {
+ private final OperationCase operationCase;
+ private final Object operation;
+ private final ByteString start;
+ private final ByteString limit;
+ private final boolean approximate;
+
+ public enum OperationCase {
+ KEY,
+ KEY_RANGE,
+ KEY_SET,
+ MUTATION,
+ QUERY_PARAMS,
+ OPERATION_NOT_SET
+ }
+
+ private Test(Builder builder) {
+ this.operationCase = builder.operationCase;
+ this.operation = builder.operation;
+ this.start = builder.start;
+ this.limit = builder.limit;
+ this.approximate = builder.approximate;
+ }
+
+ public OperationCase getOperationCase() {
+ return operationCase;
+ }
+
+ public ListValue getKey() {
+ if (operationCase == OperationCase.KEY) {
+ return (ListValue) operation;
+ }
+ return ListValue.getDefaultInstance();
+ }
+
+ public KeyRange getKeyRange() {
+ if (operationCase == OperationCase.KEY_RANGE) {
+ return (KeyRange) operation;
+ }
+ return KeyRange.getDefaultInstance();
+ }
+
+ public KeySet getKeySet() {
+ if (operationCase == OperationCase.KEY_SET) {
+ return (KeySet) operation;
+ }
+ return KeySet.getDefaultInstance();
+ }
+
+ public Mutation getMutation() {
+ if (operationCase == OperationCase.MUTATION) {
+ return (Mutation) operation;
+ }
+ return Mutation.getDefaultInstance();
+ }
+
+ public Struct getQueryParams() {
+ if (operationCase == OperationCase.QUERY_PARAMS) {
+ return (Struct) operation;
+ }
+ return Struct.getDefaultInstance();
+ }
+
+ public ByteString getStart() {
+ return start;
+ }
+
+ public ByteString getLimit() {
+ return limit;
+ }
+
+ public boolean getApproximate() {
+ return approximate;
+ }
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ public static final class Builder {
+ private OperationCase operationCase = OperationCase.OPERATION_NOT_SET;
+ private Object operation;
+ private ByteString start;
+ private ByteString limit;
+ private boolean approximate;
+
+ public Builder setKey(ListValue key) {
+ this.operationCase = OperationCase.KEY;
+ this.operation = key;
+ return this;
+ }
+
+ public Builder setKeyRange(KeyRange keyRange) {
+ this.operationCase = OperationCase.KEY_RANGE;
+ this.operation = keyRange;
+ return this;
+ }
+
+ public Builder setKeySet(KeySet keySet) {
+ this.operationCase = OperationCase.KEY_SET;
+ this.operation = keySet;
+ return this;
+ }
+
+ public Builder setMutation(Mutation mutation) {
+ this.operationCase = OperationCase.MUTATION;
+ this.operation = mutation;
+ return this;
+ }
+
+ public Builder setQueryParams(Struct queryParams) {
+ this.operationCase = OperationCase.QUERY_PARAMS;
+ this.operation = queryParams;
+ return this;
+ }
+
+ public Builder setStart(ByteString start) {
+ this.start = start;
+ return this;
+ }
+
+ public Builder setLimit(ByteString limit) {
+ this.limit = limit;
+ return this;
+ }
+
+ public Builder setApproximate(boolean approximate) {
+ this.approximate = approximate;
+ return this;
+ }
+
+ public Test build() {
+ return new Test(this);
+ }
+ }
+ }
+ }
+}
diff --git a/google-cloud-spanner/src/test/resources/recipe_test.textproto b/google-cloud-spanner/src/test/resources/recipe_test.textproto
new file mode 100644
index 0000000000..43fae04f5e
--- /dev/null
+++ b/google-cloud-spanner/src/test/resources/recipe_test.textproto
@@ -0,0 +1,3943 @@
+test_case {
+ name: "DataTypeTest_BOOL"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "DataTypeTest_BOOL"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: BOOL
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ bool_value: false
+ }
+ }
+ start: "A\206\310\002\234\200\000"
+ }
+ test {
+ key {
+ values {
+ bool_value: true
+ }
+ }
+ start: "A\206\310\002\234\200\002"
+ }
+ test {
+ key {
+ values {
+ null_value: NULL_VALUE
+ }
+ }
+ start: "A\206\310\002\233\000"
+ }
+ test {
+ key {
+ values {
+ string_value: "true"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ number_value: 0
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ bool_value: false
+ }
+ }
+ end_open {
+ values {
+ bool_value: true
+ }
+ }
+ }
+ start: "A\206\310\002\234\200\000"
+ limit: "A\206\310\002\234\200\002"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ bool_value: false
+ }
+ }
+ end_closed {
+ values {
+ bool_value: true
+ }
+ }
+ }
+ start: "A\206\310\002\234\200\001"
+ limit: "A\206\310\002\234\200\003"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ bool_value: false
+ }
+ }
+ end_closed {
+ values {
+ bool_value: true
+ }
+ }
+ }
+ start: "A\206\310\002\234\200\000"
+ limit: "A\206\310\002\234\200\003"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ bool_value: false
+ }
+ }
+ end_open {
+ values {
+ bool_value: true
+ }
+ }
+ }
+ start: "A\206\310\002\234\200\001"
+ limit: "A\206\310\002\234\200\002"
+ }
+}
+
+test_case {
+ name: "DataTypeTest_BOOL_Desc"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "DataTypeTest_BOOL_Desc"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: DESCENDING
+ null_order: NULLS_LAST
+ type {
+ code: BOOL
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ bool_value: true
+ }
+ }
+ start: "A\206\310\002\273\250\374"
+ }
+ test {
+ key {
+ values {
+ bool_value: false
+ }
+ }
+ start: "A\206\310\002\273\250\376"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ bool_value: true
+ }
+ }
+ end_open {
+ values {
+ bool_value: false
+ }
+ }
+ }
+ start: "A\206\310\002\273\250\374"
+ limit: "A\206\310\002\273\250\376"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ bool_value: true
+ }
+ }
+ end_closed {
+ values {
+ bool_value: false
+ }
+ }
+ }
+ start: "A\206\310\002\273\250\375"
+ limit: "A\206\310\002\273\250\377"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ bool_value: true
+ }
+ }
+ end_closed {
+ values {
+ bool_value: false
+ }
+ }
+ }
+ start: "A\206\310\002\273\250\374"
+ limit: "A\206\310\002\273\250\377"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ bool_value: true
+ }
+ }
+ end_open {
+ values {
+ bool_value: false
+ }
+ }
+ }
+ start: "A\206\310\002\273\250\375"
+ limit: "A\206\310\002\273\250\376"
+ }
+}
+
+test_case {
+ name: "DataTypeTest_ENUM"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "DataTypeTest_ENUM"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: ENUM
+ proto_type_fqn: "spanner.test.TestEnum"
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "1"
+ }
+ }
+ start: "A\206\310\002\234\221\002"
+ }
+ test {
+ key {
+ values {
+ string_value: "2"
+ }
+ }
+ start: "A\206\310\002\234\221\004"
+ }
+ test {
+ key {
+ values {
+ null_value: NULL_VALUE
+ }
+ }
+ start: "A\206\310\002\233\000"
+ }
+ test {
+ key {
+ values {
+ string_value: "NUMBER_ONE"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ number_value: 0
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "1"
+ }
+ }
+ end_open {
+ values {
+ string_value: "2"
+ }
+ }
+ }
+ start: "A\206\310\002\234\221\002"
+ limit: "A\206\310\002\234\221\004"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "1"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "2"
+ }
+ }
+ }
+ start: "A\206\310\002\234\221\003"
+ limit: "A\206\310\002\234\221\005"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "1"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "2"
+ }
+ }
+ }
+ start: "A\206\310\002\234\221\002"
+ limit: "A\206\310\002\234\221\005"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "1"
+ }
+ }
+ end_open {
+ values {
+ string_value: "2"
+ }
+ }
+ }
+ start: "A\206\310\002\234\221\003"
+ limit: "A\206\310\002\234\221\004"
+ }
+}
+
+test_case {
+ name: "DataTypeTest_ENUM_Desc"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "DataTypeTest_ENUM_Desc"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: DESCENDING
+ null_order: NULLS_LAST
+ type {
+ code: ENUM
+ proto_type_fqn: "spanner.test.TestEnum"
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "2"
+ }
+ }
+ start: "A\206\310\002\273\260\372"
+ }
+ test {
+ key {
+ values {
+ string_value: "1"
+ }
+ }
+ start: "A\206\310\002\273\260\374"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "2"
+ }
+ }
+ end_open {
+ values {
+ string_value: "1"
+ }
+ }
+ }
+ start: "A\206\310\002\273\260\372"
+ limit: "A\206\310\002\273\260\374"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "2"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "1"
+ }
+ }
+ }
+ start: "A\206\310\002\273\260\373"
+ limit: "A\206\310\002\273\260\375"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "2"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "1"
+ }
+ }
+ }
+ start: "A\206\310\002\273\260\372"
+ limit: "A\206\310\002\273\260\375"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "2"
+ }
+ }
+ end_open {
+ values {
+ string_value: "1"
+ }
+ }
+ }
+ start: "A\206\310\002\273\260\373"
+ limit: "A\206\310\002\273\260\374"
+ }
+}
+
+test_case {
+ name: "DataTypeTest_INT64"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "DataTypeTest_INT64"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: INT64
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "-9223372036854775808"
+ }
+ }
+ start: "A\206\310\002\234\211\000\000\000\000\000\000\000\000"
+ }
+ test {
+ key {
+ values {
+ string_value: "9223372036854775807"
+ }
+ }
+ start: "A\206\310\002\234\230\377\377\377\377\377\377\377\376"
+ }
+ test {
+ key {
+ values {
+ null_value: NULL_VALUE
+ }
+ }
+ start: "A\206\310\002\233\000"
+ }
+ test {
+ key {
+ values {
+ string_value: "0"
+ }
+ }
+ start: "A\206\310\002\234\221\000"
+ }
+ test {
+ key {
+ values {
+ string_value: "-1"
+ }
+ }
+ start: "A\206\310\002\234\220\376"
+ }
+ test {
+ key {
+ values {
+ string_value: "1"
+ }
+ }
+ start: "A\206\310\002\234\221\002"
+ }
+ test {
+ key {
+ values {
+ number_value: 1
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "Infinity"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "-9223372036854775808"
+ }
+ }
+ end_open {
+ values {
+ string_value: "9223372036854775807"
+ }
+ }
+ }
+ start: "A\206\310\002\234\211\000\000\000\000\000\000\000\000"
+ limit: "A\206\310\002\234\230\377\377\377\377\377\377\377\376"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "-9223372036854775808"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "9223372036854775807"
+ }
+ }
+ }
+ start: "A\206\310\002\234\211\000\000\000\000\000\000\000\001"
+ limit: "A\206\310\002\234\230\377\377\377\377\377\377\377\377"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "-9223372036854775808"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "9223372036854775807"
+ }
+ }
+ }
+ start: "A\206\310\002\234\211\000\000\000\000\000\000\000\000"
+ limit: "A\206\310\002\234\230\377\377\377\377\377\377\377\377"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "-9223372036854775808"
+ }
+ }
+ end_open {
+ values {
+ string_value: "9223372036854775807"
+ }
+ }
+ }
+ start: "A\206\310\002\234\211\000\000\000\000\000\000\000\001"
+ limit: "A\206\310\002\234\230\377\377\377\377\377\377\377\376"
+ }
+}
+
+test_case {
+ name: "DataTypeTest_INT64_Desc"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "DataTypeTest_INT64_Desc"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: DESCENDING
+ null_order: NULLS_LAST
+ type {
+ code: INT64
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "9223372036854775807"
+ }
+ }
+ start: "A\206\310\002\273\251\000\000\000\000\000\000\000\000"
+ }
+ test {
+ key {
+ values {
+ string_value: "-9223372036854775808"
+ }
+ }
+ start: "A\206\310\002\273\270\377\377\377\377\377\377\377\376"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "9223372036854775807"
+ }
+ }
+ end_open {
+ values {
+ string_value: "-9223372036854775808"
+ }
+ }
+ }
+ start: "A\206\310\002\273\251\000\000\000\000\000\000\000\000"
+ limit: "A\206\310\002\273\270\377\377\377\377\377\377\377\376"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "9223372036854775807"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "-9223372036854775808"
+ }
+ }
+ }
+ start: "A\206\310\002\273\251\000\000\000\000\000\000\000\001"
+ limit: "A\206\310\002\273\270\377\377\377\377\377\377\377\377"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "9223372036854775807"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "-9223372036854775808"
+ }
+ }
+ }
+ start: "A\206\310\002\273\251\000\000\000\000\000\000\000\000"
+ limit: "A\206\310\002\273\270\377\377\377\377\377\377\377\377"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "9223372036854775807"
+ }
+ }
+ end_open {
+ values {
+ string_value: "-9223372036854775808"
+ }
+ }
+ }
+ start: "A\206\310\002\273\251\000\000\000\000\000\000\000\001"
+ limit: "A\206\310\002\273\270\377\377\377\377\377\377\377\376"
+ }
+}
+
+test_case {
+ name: "DataTypeTest_FLOAT64"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "DataTypeTest_FLOAT64"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: FLOAT64
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ number_value: -1.7976931348623157e+308
+ }
+ }
+ start: "A\206\310\002\234\302\000 \000\000\000\000\000\002"
+ }
+ test {
+ key {
+ values {
+ number_value: 1.7976931348623157e+308
+ }
+ }
+ start: "A\206\310\002\234\321\377\337\377\377\377\377\377\376"
+ }
+ test {
+ key {
+ values {
+ null_value: NULL_VALUE
+ }
+ }
+ start: "A\206\310\002\233\000"
+ }
+ test {
+ key {
+ values {
+ number_value: 0
+ }
+ }
+ start: "A\206\310\002\234\312\000"
+ }
+ test {
+ key {
+ values {
+ number_value: -1
+ }
+ }
+ start: "A\206\310\002\234\302\200 \000\000\000\000\000\000"
+ }
+ test {
+ key {
+ values {
+ number_value: 1
+ }
+ }
+ start: "A\206\310\002\234\321\177\340\000\000\000\000\000\000"
+ }
+ test {
+ key {
+ values {
+ string_value: "Infinity"
+ }
+ }
+ start: "A\206\310\002\234\321\377\340\000\000\000\000\000\000"
+ }
+ test {
+ key {
+ values {
+ string_value: "-Infinity"
+ }
+ }
+ start: "A\206\310\002\234\302\000 \000\000\000\000\000\000"
+ }
+ test {
+ key {
+ values {
+ string_value: "NaN"
+ }
+ }
+ start: "A\206\310\002\234\321\377\360\000\000\000\000\000\000"
+ }
+ test {
+ key {
+ values {
+ string_value: "UnexpectedString"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ bool_value: true
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ number_value: -1.7976931348623157e+308
+ }
+ }
+ end_open {
+ values {
+ number_value: 1.7976931348623157e+308
+ }
+ }
+ }
+ start: "A\206\310\002\234\302\000 \000\000\000\000\000\002"
+ limit: "A\206\310\002\234\321\377\337\377\377\377\377\377\376"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ number_value: -1.7976931348623157e+308
+ }
+ }
+ end_closed {
+ values {
+ number_value: 1.7976931348623157e+308
+ }
+ }
+ }
+ start: "A\206\310\002\234\302\000 \000\000\000\000\000\003"
+ limit: "A\206\310\002\234\321\377\337\377\377\377\377\377\377"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ number_value: -1.7976931348623157e+308
+ }
+ }
+ end_closed {
+ values {
+ number_value: 1.7976931348623157e+308
+ }
+ }
+ }
+ start: "A\206\310\002\234\302\000 \000\000\000\000\000\002"
+ limit: "A\206\310\002\234\321\377\337\377\377\377\377\377\377"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ number_value: -1.7976931348623157e+308
+ }
+ }
+ end_open {
+ values {
+ number_value: 1.7976931348623157e+308
+ }
+ }
+ }
+ start: "A\206\310\002\234\302\000 \000\000\000\000\000\003"
+ limit: "A\206\310\002\234\321\377\337\377\377\377\377\377\376"
+ }
+}
+
+test_case {
+ name: "DataTypeTest_FLOAT64_Desc"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "DataTypeTest_FLOAT64_Desc"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: DESCENDING
+ null_order: NULLS_LAST
+ type {
+ code: FLOAT64
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ number_value: 1.7976931348623157e+308
+ }
+ }
+ start: "A\206\310\002\273\322\000 \000\000\000\000\000\000"
+ }
+ test {
+ key {
+ values {
+ number_value: -1.7976931348623157e+308
+ }
+ }
+ start: "A\206\310\002\273\341\377\337\377\377\377\377\377\374"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ number_value: 1.7976931348623157e+308
+ }
+ }
+ end_open {
+ values {
+ number_value: -1.7976931348623157e+308
+ }
+ }
+ }
+ start: "A\206\310\002\273\322\000 \000\000\000\000\000\000"
+ limit: "A\206\310\002\273\341\377\337\377\377\377\377\377\374"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ number_value: 1.7976931348623157e+308
+ }
+ }
+ end_closed {
+ values {
+ number_value: -1.7976931348623157e+308
+ }
+ }
+ }
+ start: "A\206\310\002\273\322\000 \000\000\000\000\000\001"
+ limit: "A\206\310\002\273\341\377\337\377\377\377\377\377\375"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ number_value: 1.7976931348623157e+308
+ }
+ }
+ end_closed {
+ values {
+ number_value: -1.7976931348623157e+308
+ }
+ }
+ }
+ start: "A\206\310\002\273\322\000 \000\000\000\000\000\000"
+ limit: "A\206\310\002\273\341\377\337\377\377\377\377\377\375"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ number_value: 1.7976931348623157e+308
+ }
+ }
+ end_open {
+ values {
+ number_value: -1.7976931348623157e+308
+ }
+ }
+ }
+ start: "A\206\310\002\273\322\000 \000\000\000\000\000\001"
+ limit: "A\206\310\002\273\341\377\337\377\377\377\377\377\374"
+ }
+}
+
+test_case {
+ name: "DataTypeTest_TIMESTAMP"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "DataTypeTest_TIMESTAMP"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: TIMESTAMP
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "0001-01-01T00:00:00Z"
+ }
+ }
+ start: "A\206\310\002\234\231\177\377\020\377\020\361\210n\t\000\360\000\360\000\360\000\360\000\360\000x"
+ }
+ test {
+ key {
+ values {
+ string_value: "9999-12-31T23:59:59.999999999Z"
+ }
+ }
+ start: "A\206\310\002\234\231\200\000\360\000\360:\377\020\364A\177;\232\311\377\020\000x"
+ }
+ test {
+ key {
+ values {
+ null_value: NULL_VALUE
+ }
+ }
+ start: "A\206\310\002\233\000"
+ }
+ test {
+ key {
+ values {
+ string_value: "1970-01-01T00:00:00Z"
+ }
+ }
+ start: "A\206\310\002\234\231\200\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000x"
+ }
+ test {
+ key {
+ values {
+ string_value: "2023-10-26T10:00:00Z"
+ }
+ }
+ start: "A\206\310\002\234\231\200\000\360\000\360\000\360e:8\240\000\360\000\360\000\360\000\360\000x"
+ }
+ test {
+ key {
+ values {
+ string_value: "2023-10-26T10:00:00.1234567890Z"
+ }
+ }
+ start: "A\206\310\002\234\231\200\000\360\000\360\000\360e:8\240\007[\315\025\000x"
+ }
+ test {
+ key {
+ values {
+ string_value: "2023-10-26T10:00:00.1234567891Z"
+ }
+ }
+ start: "A\206\310\002\234\231\200\000\360\000\360\000\360e:8\240\007[\315\025\000x"
+ }
+ test {
+ key {
+ values {
+ string_value: "2023-10-26T10:00:00.1234567899Z"
+ }
+ }
+ start: "A\206\310\002\234\231\200\000\360\000\360\000\360e:8\240\007[\315\025\000x"
+ }
+ test {
+ key {
+ values {
+ string_value: "0000-10-26T10:00:00Z"
+ }
+ }
+ start: "A\206\310\002\234\231\177\377\020\377\020\361\210\026A \000\360\000\360\000\360\000\360\000x"
+ }
+ test {
+ key {
+ values {
+ string_value: "NOT A TIMESTAMP"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ number_value: 0
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "2023-10-26T10:00:00"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "2023-10-26T10:00:00z"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "2023-10-26T10:00:00+07:00"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "2023-13-26T10:00:00Z"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "2023-10-26T10:00:61Z"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "2023-10-26 10:00:00Z"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "10000-10-26T10:00:00Z"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "0001-01-01T00:00:00Z"
+ }
+ }
+ end_open {
+ values {
+ string_value: "9999-12-31T23:59:59.999999999Z"
+ }
+ }
+ }
+ start: "A\206\310\002\234\231\177\377\020\377\020\361\210n\t\000\360\000\360\000\360\000\360\000\360\000x"
+ limit: "A\206\310\002\234\231\200\000\360\000\360:\377\020\364A\177;\232\311\377\020\000x"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "0001-01-01T00:00:00Z"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "9999-12-31T23:59:59.999999999Z"
+ }
+ }
+ }
+ start: "A\206\310\002\234\231\177\377\020\377\020\361\210n\t\000\360\000\360\000\360\000\360\000\360\000y"
+ limit: "A\206\310\002\234\231\200\000\360\000\360:\377\020\364A\177;\232\311\377\020\000y"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "0001-01-01T00:00:00Z"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "9999-12-31T23:59:59.999999999Z"
+ }
+ }
+ }
+ start: "A\206\310\002\234\231\177\377\020\377\020\361\210n\t\000\360\000\360\000\360\000\360\000\360\000x"
+ limit: "A\206\310\002\234\231\200\000\360\000\360:\377\020\364A\177;\232\311\377\020\000y"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "0001-01-01T00:00:00Z"
+ }
+ }
+ end_open {
+ values {
+ string_value: "9999-12-31T23:59:59.999999999Z"
+ }
+ }
+ }
+ start: "A\206\310\002\234\231\177\377\020\377\020\361\210n\t\000\360\000\360\000\360\000\360\000\360\000y"
+ limit: "A\206\310\002\234\231\200\000\360\000\360:\377\020\364A\177;\232\311\377\020\000x"
+ }
+}
+
+test_case {
+ name: "DataTypeTest_TIMESTAMP_Desc"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "DataTypeTest_TIMESTAMP_Desc"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: DESCENDING
+ null_order: NULLS_LAST
+ type {
+ code: TIMESTAMP
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "9999-12-31T23:59:59.999999999Z"
+ }
+ }
+ start: "A\206\310\002\273\271\177\377\020\377\020\305\000\360\013\276\200\304e6\000\360\377x"
+ }
+ test {
+ key {
+ values {
+ string_value: "0001-01-01T00:00:00Z"
+ }
+ }
+ start: "A\206\310\002\273\271\200\000\360\000\360\016w\221\366\377\020\377\020\377\020\377\020\377\020\377x"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "9999-12-31T23:59:59.999999999Z"
+ }
+ }
+ end_open {
+ values {
+ string_value: "0001-01-01T00:00:00Z"
+ }
+ }
+ }
+ start: "A\206\310\002\273\271\177\377\020\377\020\305\000\360\013\276\200\304e6\000\360\377x"
+ limit: "A\206\310\002\273\271\200\000\360\000\360\016w\221\366\377\020\377\020\377\020\377\020\377\020\377x"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "9999-12-31T23:59:59.999999999Z"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "0001-01-01T00:00:00Z"
+ }
+ }
+ }
+ start: "A\206\310\002\273\271\177\377\020\377\020\305\000\360\013\276\200\304e6\000\360\377y"
+ limit: "A\206\310\002\273\271\200\000\360\000\360\016w\221\366\377\020\377\020\377\020\377\020\377\020\377y"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "9999-12-31T23:59:59.999999999Z"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "0001-01-01T00:00:00Z"
+ }
+ }
+ }
+ start: "A\206\310\002\273\271\177\377\020\377\020\305\000\360\013\276\200\304e6\000\360\377x"
+ limit: "A\206\310\002\273\271\200\000\360\000\360\016w\221\366\377\020\377\020\377\020\377\020\377\020\377y"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "9999-12-31T23:59:59.999999999Z"
+ }
+ }
+ end_open {
+ values {
+ string_value: "0001-01-01T00:00:00Z"
+ }
+ }
+ }
+ start: "A\206\310\002\273\271\177\377\020\377\020\305\000\360\013\276\200\304e6\000\360\377y"
+ limit: "A\206\310\002\273\271\200\000\360\000\360\016w\221\366\377\020\377\020\377\020\377\020\377\020\377x"
+ }
+}
+
+test_case {
+ name: "DataTypeTest_DATE"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "DataTypeTest_DATE"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: DATE
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "0000-01-01"
+ }
+ }
+ start: "A\206\310\002\234\216\352\n\260"
+ }
+ test {
+ key {
+ values {
+ string_value: "9999-12-31"
+ }
+ }
+ start: "A\206\310\002\234\223Y\201@"
+ }
+ test {
+ key {
+ values {
+ null_value: NULL_VALUE
+ }
+ }
+ start: "A\206\310\002\233\000"
+ }
+ test {
+ key {
+ values {
+ string_value: "1970-01-01"
+ }
+ }
+ start: "A\206\310\002\234\221\000"
+ }
+ test {
+ key {
+ values {
+ string_value: "2023-10-26"
+ }
+ }
+ start: "A\206\310\002\234\222\231\220"
+ }
+ test {
+ key {
+ values {
+ string_value: "NOT A DATE"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ number_value: 0
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "2023-13-01"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "2023-12-32"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "10000-01-01"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "2023-1-1"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "2023-01-001"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "2023/01/01"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "2023-01-01T10:00:00Z"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "0000-01-01"
+ }
+ }
+ end_open {
+ values {
+ string_value: "9999-12-31"
+ }
+ }
+ }
+ start: "A\206\310\002\234\216\352\n\260"
+ limit: "A\206\310\002\234\223Y\201@"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "0000-01-01"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "9999-12-31"
+ }
+ }
+ }
+ start: "A\206\310\002\234\216\352\n\261"
+ limit: "A\206\310\002\234\223Y\201A"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "0000-01-01"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "9999-12-31"
+ }
+ }
+ }
+ start: "A\206\310\002\234\216\352\n\260"
+ limit: "A\206\310\002\234\223Y\201A"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "0000-01-01"
+ }
+ }
+ end_open {
+ values {
+ string_value: "9999-12-31"
+ }
+ }
+ }
+ start: "A\206\310\002\234\216\352\n\261"
+ limit: "A\206\310\002\234\223Y\201@"
+ }
+}
+
+test_case {
+ name: "DataTypeTest_DATE_Desc"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "DataTypeTest_DATE_Desc"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: DESCENDING
+ null_order: NULLS_LAST
+ type {
+ code: DATE
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "9999-12-31"
+ }
+ }
+ start: "A\206\310\002\273\256\246~\276"
+ }
+ test {
+ key {
+ values {
+ string_value: "0000-01-01"
+ }
+ }
+ start: "A\206\310\002\273\263\025\365N"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "9999-12-31"
+ }
+ }
+ end_open {
+ values {
+ string_value: "0000-01-01"
+ }
+ }
+ }
+ start: "A\206\310\002\273\256\246~\276"
+ limit: "A\206\310\002\273\263\025\365N"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "9999-12-31"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "0000-01-01"
+ }
+ }
+ }
+ start: "A\206\310\002\273\256\246~\277"
+ limit: "A\206\310\002\273\263\025\365O"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "9999-12-31"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "0000-01-01"
+ }
+ }
+ }
+ start: "A\206\310\002\273\256\246~\276"
+ limit: "A\206\310\002\273\263\025\365O"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "9999-12-31"
+ }
+ }
+ end_open {
+ values {
+ string_value: "0000-01-01"
+ }
+ }
+ }
+ start: "A\206\310\002\273\256\246~\277"
+ limit: "A\206\310\002\273\263\025\365N"
+ }
+}
+
+test_case {
+ name: "DataTypeTest_STRING"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "DataTypeTest_STRING"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: STRING
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: ""
+ }
+ }
+ start: "A\206\310\002\234\231\000x"
+ }
+ test {
+ key {
+ values {
+ string_value: "ZZZZZZZ"
+ }
+ }
+ start: "A\206\310\002\234\231ZZZZZZZ\000x"
+ }
+ test {
+ key {
+ values {
+ null_value: NULL_VALUE
+ }
+ }
+ start: "A\206\310\002\233\000"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: ""
+ }
+ }
+ end_open {
+ values {
+ string_value: "ZZZZZZZ"
+ }
+ }
+ }
+ start: "A\206\310\002\234\231\000x"
+ limit: "A\206\310\002\234\231ZZZZZZZ\000x"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: ""
+ }
+ }
+ end_closed {
+ values {
+ string_value: "ZZZZZZZ"
+ }
+ }
+ }
+ start: "A\206\310\002\234\231\000y"
+ limit: "A\206\310\002\234\231ZZZZZZZ\000y"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: ""
+ }
+ }
+ end_closed {
+ values {
+ string_value: "ZZZZZZZ"
+ }
+ }
+ }
+ start: "A\206\310\002\234\231\000x"
+ limit: "A\206\310\002\234\231ZZZZZZZ\000y"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: ""
+ }
+ }
+ end_open {
+ values {
+ string_value: "ZZZZZZZ"
+ }
+ }
+ }
+ start: "A\206\310\002\234\231\000y"
+ limit: "A\206\310\002\234\231ZZZZZZZ\000x"
+ }
+}
+
+test_case {
+ name: "DataTypeTest_STRING_Desc"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "DataTypeTest_STRING_Desc"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: DESCENDING
+ null_order: NULLS_LAST
+ type {
+ code: STRING
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "ZZZZZZZ"
+ }
+ }
+ start: "A\206\310\002\273\271\245\245\245\245\245\245\245\377x"
+ }
+ test {
+ key {
+ values {
+ string_value: ""
+ }
+ }
+ start: "A\206\310\002\273\271\377x"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "ZZZZZZZ"
+ }
+ }
+ end_open {
+ values {
+ string_value: ""
+ }
+ }
+ }
+ start: "A\206\310\002\273\271\245\245\245\245\245\245\245\377x"
+ limit: "A\206\310\002\273\271\377x"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "ZZZZZZZ"
+ }
+ }
+ end_closed {
+ values {
+ string_value: ""
+ }
+ }
+ }
+ start: "A\206\310\002\273\271\245\245\245\245\245\245\245\377y"
+ limit: "A\206\310\002\273\271\377y"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "ZZZZZZZ"
+ }
+ }
+ end_closed {
+ values {
+ string_value: ""
+ }
+ }
+ }
+ start: "A\206\310\002\273\271\245\245\245\245\245\245\245\377x"
+ limit: "A\206\310\002\273\271\377y"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "ZZZZZZZ"
+ }
+ }
+ end_open {
+ values {
+ string_value: ""
+ }
+ }
+ }
+ start: "A\206\310\002\273\271\245\245\245\245\245\245\245\377y"
+ limit: "A\206\310\002\273\271\377x"
+ }
+}
+
+test_case {
+ name: "DataTypeTest_BYTES"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "DataTypeTest_BYTES"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: BYTES
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: ""
+ }
+ }
+ start: "A\206\310\002\234\231\000x"
+ }
+ test {
+ key {
+ values {
+ string_value: "/////w=="
+ }
+ }
+ start: "A\206\310\002\234\231\377\020\377\020\377\020\377\020\000x"
+ }
+ test {
+ key {
+ values {
+ null_value: NULL_VALUE
+ }
+ }
+ start: "A\206\310\002\233\000"
+ }
+ test {
+ key {
+ values {
+ string_value: ""
+ }
+ }
+ start: "A\206\310\002\234\231\000x"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: ""
+ }
+ }
+ end_open {
+ values {
+ string_value: "/////w=="
+ }
+ }
+ }
+ start: "A\206\310\002\234\231\000x"
+ limit: "A\206\310\002\234\231\377\020\377\020\377\020\377\020\000x"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: ""
+ }
+ }
+ end_closed {
+ values {
+ string_value: "/////w=="
+ }
+ }
+ }
+ start: "A\206\310\002\234\231\000y"
+ limit: "A\206\310\002\234\231\377\020\377\020\377\020\377\020\000y"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: ""
+ }
+ }
+ end_closed {
+ values {
+ string_value: "/////w=="
+ }
+ }
+ }
+ start: "A\206\310\002\234\231\000x"
+ limit: "A\206\310\002\234\231\377\020\377\020\377\020\377\020\000y"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: ""
+ }
+ }
+ end_open {
+ values {
+ string_value: "/////w=="
+ }
+ }
+ }
+ start: "A\206\310\002\234\231\000y"
+ limit: "A\206\310\002\234\231\377\020\377\020\377\020\377\020\000x"
+ }
+}
+
+test_case {
+ name: "DataTypeTest_BYTES_Desc"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "DataTypeTest_BYTES_Desc"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: DESCENDING
+ null_order: NULLS_LAST
+ type {
+ code: BYTES
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "/////w=="
+ }
+ }
+ start: "A\206\310\002\273\271\000\360\000\360\000\360\000\360\377x"
+ }
+ test {
+ key {
+ values {
+ string_value: ""
+ }
+ }
+ start: "A\206\310\002\273\271\377x"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "/////w=="
+ }
+ }
+ end_open {
+ values {
+ string_value: ""
+ }
+ }
+ }
+ start: "A\206\310\002\273\271\000\360\000\360\000\360\000\360\377x"
+ limit: "A\206\310\002\273\271\377x"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "/////w=="
+ }
+ }
+ end_closed {
+ values {
+ string_value: ""
+ }
+ }
+ }
+ start: "A\206\310\002\273\271\000\360\000\360\000\360\000\360\377y"
+ limit: "A\206\310\002\273\271\377y"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "/////w=="
+ }
+ }
+ end_closed {
+ values {
+ string_value: ""
+ }
+ }
+ }
+ start: "A\206\310\002\273\271\000\360\000\360\000\360\000\360\377x"
+ limit: "A\206\310\002\273\271\377y"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "/////w=="
+ }
+ }
+ end_open {
+ values {
+ string_value: ""
+ }
+ }
+ }
+ start: "A\206\310\002\273\271\000\360\000\360\000\360\000\360\377y"
+ limit: "A\206\310\002\273\271\377x"
+ }
+}
+
+test_case {
+ name: "NumericBasic"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "NumericBasic"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: NUMERIC
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "123"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ number_value: 123
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+}
+
+test_case {
+ name: "NumericMultiPart"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "NumericMultiPart"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: INT64
+ }
+ identifier: "user_id"
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: NUMERIC
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "123"
+ }
+ values {
+ string_value: "456"
+ }
+ }
+ start: "A\206\310\002\234\221\366"
+ limit: "A\206\310\002\234\221\367"
+ approximate: true
+ }
+}
+
+test_case {
+ name: "DataTypeTest_UUID"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "DataTypeTest_UUID"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: UUID
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "00000000-0000-0000-0000-000000000000"
+ }
+ }
+ start: "A\206\310\002\234\231\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000x"
+ }
+ test {
+ key {
+ values {
+ string_value: "ffffffff-ffff-ffff-ffff-ffffffffffff"
+ }
+ }
+ start: "A\206\310\002\234\231\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\000x"
+ }
+ test {
+ key {
+ values {
+ null_value: NULL_VALUE
+ }
+ }
+ start: "A\206\310\002\233\000"
+ }
+ test {
+ key {
+ values {
+ string_value: "00000000-0000-0000-0000-000000000000"
+ }
+ }
+ start: "A\206\310\002\234\231\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000x"
+ }
+ test {
+ key {
+ values {
+ string_value: "ffffffff-ffff-ffff-ffff-ffffffffffff"
+ }
+ }
+ start: "A\206\310\002\234\231\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\000x"
+ }
+ test {
+ key {
+ values {
+ string_value: "12345678-1234-1234-1234-1234567890ab"
+ }
+ }
+ start: "A\206\310\002\234\231\0224Vx\0224\0224\0224\0224Vx\220\253\000x"
+ }
+ test {
+ key {
+ values {
+ string_value: "12345678-1234-1234-1234-1234567890AB"
+ }
+ }
+ start: "A\206\310\002\234\231\0224Vx\0224\0224\0224\0224Vx\220\253\000x"
+ }
+ test {
+ key {
+ values {
+ string_value: "{12345678-1234-1234-1234-1234567890ad}"
+ }
+ }
+ start: "A\206\310\002\234\231\0224Vx\0224\0224\0224\0224Vx\220\255\000x"
+ }
+ test {
+ key {
+ values {
+ string_value: "{FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF}"
+ }
+ }
+ start: "A\206\310\002\234\231\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\000x"
+ }
+ test {
+ key {
+ values {
+ string_value: "NOT A UUID"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ number_value: 0
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "12345678x1234-1234-1234-1234567890ab"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "12345678-1234-1234-1234-1234567890a"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "12345678-1234-1234-1234-1234567890abc"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "12345678-1234-1234-1234-1234567890ag"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "123456781234-1234-1234-1234567890ab"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "12345678-12341234-1234-1234567890ab"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "12345678-1234-12341234-1234567890ab"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "12345678-1234-1234-12341234567890ab"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "-12345678-1234-1234-1234-1234567890ab"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "12345678-1234-1234-1234-1234567890ab-"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "12345678--1234-1234-1234-1234567890ab"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "{12345678-1234-1234-1234-1234567890ab"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "12345678-1234-1234-1234-1234567890ab}"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "{{12345678-1234-1234-1234-1234567890ab}}"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key {
+ values {
+ string_value: "12345678-{1234-1234-1234}-1234567890ab"
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "00000000-0000-0000-0000-000000000000"
+ }
+ }
+ end_open {
+ values {
+ string_value: "ffffffff-ffff-ffff-ffff-ffffffffffff"
+ }
+ }
+ }
+ start: "A\206\310\002\234\231\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000x"
+ limit: "A\206\310\002\234\231\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\000x"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "00000000-0000-0000-0000-000000000000"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "ffffffff-ffff-ffff-ffff-ffffffffffff"
+ }
+ }
+ }
+ start: "A\206\310\002\234\231\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000y"
+ limit: "A\206\310\002\234\231\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\000y"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "00000000-0000-0000-0000-000000000000"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "ffffffff-ffff-ffff-ffff-ffffffffffff"
+ }
+ }
+ }
+ start: "A\206\310\002\234\231\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000x"
+ limit: "A\206\310\002\234\231\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\000y"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "00000000-0000-0000-0000-000000000000"
+ }
+ }
+ end_open {
+ values {
+ string_value: "ffffffff-ffff-ffff-ffff-ffffffffffff"
+ }
+ }
+ }
+ start: "A\206\310\002\234\231\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000y"
+ limit: "A\206\310\002\234\231\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\000x"
+ }
+}
+
+test_case {
+ name: "DataTypeTest_UUID_Desc"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "DataTypeTest_UUID_Desc"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: DESCENDING
+ null_order: NULLS_LAST
+ type {
+ code: UUID
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "ffffffff-ffff-ffff-ffff-ffffffffffff"
+ }
+ }
+ start: "A\206\310\002\273\271\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\377x"
+ }
+ test {
+ key {
+ values {
+ string_value: "00000000-0000-0000-0000-000000000000"
+ }
+ }
+ start: "A\206\310\002\273\271\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377x"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "ffffffff-ffff-ffff-ffff-ffffffffffff"
+ }
+ }
+ end_open {
+ values {
+ string_value: "00000000-0000-0000-0000-000000000000"
+ }
+ }
+ }
+ start: "A\206\310\002\273\271\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\377x"
+ limit: "A\206\310\002\273\271\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377x"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "ffffffff-ffff-ffff-ffff-ffffffffffff"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "00000000-0000-0000-0000-000000000000"
+ }
+ }
+ }
+ start: "A\206\310\002\273\271\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\377y"
+ limit: "A\206\310\002\273\271\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377y"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "ffffffff-ffff-ffff-ffff-ffffffffffff"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "00000000-0000-0000-0000-000000000000"
+ }
+ }
+ }
+ start: "A\206\310\002\273\271\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\377x"
+ limit: "A\206\310\002\273\271\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377y"
+ }
+ test {
+ key_range {
+ start_open {
+ values {
+ string_value: "ffffffff-ffff-ffff-ffff-ffffffffffff"
+ }
+ }
+ end_open {
+ values {
+ string_value: "00000000-0000-0000-0000-000000000000"
+ }
+ }
+ }
+ start: "A\206\310\002\273\271\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\000\360\377y"
+ limit: "A\206\310\002\273\271\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377\020\377x"
+ }
+}
+
+test_case {
+ name: "NotNull"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "NotNull"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NOT_NULL
+ type {
+ code: STRING
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: ""
+ }
+ }
+ start: "A\206\310\002\231\000x"
+ }
+ test {
+ key {
+ values {
+ string_value: "foo"
+ }
+ }
+ start: "A\206\310\002\231foo\000x"
+ }
+ test {
+ key {
+ values {
+ null_value: NULL_VALUE
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+}
+
+test_case {
+ name: "NullsLast"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "NullsLast"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_LAST
+ type {
+ code: STRING
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: ""
+ }
+ }
+ start: "A\206\310\002\273\231\000x"
+ }
+ test {
+ key {
+ values {
+ string_value: "foo"
+ }
+ }
+ start: "A\206\310\002\273\231foo\000x"
+ }
+ test {
+ key {
+ values {
+ null_value: NULL_VALUE
+ }
+ }
+ start: "A\206\310\002\274\000"
+ }
+}
+
+test_case {
+ name: "MultiPart"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "MultiPart"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: STRING
+ }
+ identifier: "k1"
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: INT64
+ }
+ identifier: "k2"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "foo"
+ }
+ values {
+ string_value: "8"
+ }
+ }
+ start: "A\206\310\002\234\231foo\000x\234\221\020"
+ }
+ test {
+ key {
+ values {
+ string_value: "foo"
+ }
+ values {
+ null_value: NULL_VALUE
+ }
+ }
+ start: "A\206\310\002\234\231foo\000x\233\000"
+ }
+ test {
+ key {
+ values {
+ null_value: NULL_VALUE
+ }
+ values {
+ string_value: "8"
+ }
+ }
+ start: "A\206\310\002\233\000\234\221\020"
+ }
+ test {
+ key {
+ values {
+ null_value: NULL_VALUE
+ }
+ values {
+ null_value: NULL_VALUE
+ }
+ }
+ start: "A\206\310\002\233\000\233\000"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "A"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "Z"
+ }
+ }
+ }
+ start: "A\206\310\002\234\231A\000x"
+ limit: "A\206\310\002\234\231Z\000y"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "A"
+ }
+ values {
+ string_value: "4"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "A"
+ }
+ values {
+ string_value: "7"
+ }
+ }
+ }
+ start: "A\206\310\002\234\231A\000x\234\221\010"
+ limit: "A\206\310\002\234\231A\000x\234\221\017"
+ }
+}
+
+test_case {
+ name: "Interleaved"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "C"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: STRING
+ }
+ identifier: "k"
+ }
+ part {
+ tag: 2
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: INT64
+ }
+ identifier: "k2"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "foo"
+ }
+ values {
+ string_value: "99"
+ }
+ }
+ start: "A\206\310\002\234\231foo\000x\004\234\221\306"
+ }
+ test {
+ key {
+ values {
+ string_value: "foo"
+ }
+ values {
+ null_value: NULL_VALUE
+ }
+ }
+ start: "A\206\310\002\234\231foo\000x\004\233\000"
+ }
+ test {
+ key {
+ values {
+ null_value: NULL_VALUE
+ }
+ values {
+ string_value: "99"
+ }
+ }
+ start: "A\206\310\002\233\000\004\234\221\306"
+ }
+ test {
+ key {
+ values {
+ null_value: NULL_VALUE
+ }
+ values {
+ null_value: NULL_VALUE
+ }
+ }
+ start: "A\206\310\002\233\000\004\233\000"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "A"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "Z"
+ }
+ }
+ }
+ start: "A\206\310\002\234\231A\000x\004"
+ limit: "A\206\310\002\234\231Z\000x\005"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "A"
+ }
+ values {
+ string_value: "4"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "A"
+ }
+ values {
+ string_value: "7"
+ }
+ }
+ }
+ start: "A\206\310\002\234\231A\000x\004\234\221\010"
+ limit: "A\206\310\002\234\231A\000x\004\234\221\017"
+ }
+}
+
+test_case {
+ name: "GeneratedKeyColumns"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "T"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: STRING
+ }
+ identifier: "k"
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: INT64
+ }
+ identifier: "k3"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "foo"
+ }
+ values {
+ string_value: "99"
+ }
+ }
+ start: "A\206\310\002\234\231foo\000x\234\221\306"
+ }
+ test {
+ key {
+ values {
+ string_value: "foo"
+ }
+ values {
+ null_value: NULL_VALUE
+ }
+ }
+ start: "A\206\310\002\234\231foo\000x\233\000"
+ }
+ test {
+ key {
+ values {
+ null_value: NULL_VALUE
+ }
+ values {
+ string_value: "99"
+ }
+ }
+ start: "A\206\310\002\233\000\234\221\306"
+ }
+ test {
+ key {
+ values {
+ null_value: NULL_VALUE
+ }
+ values {
+ null_value: NULL_VALUE
+ }
+ }
+ start: "A\206\310\002\233\000\233\000"
+ }
+ test {
+ key_range {
+ start_closed {
+ values {
+ string_value: "A"
+ }
+ values {
+ string_value: "4"
+ }
+ }
+ end_closed {
+ values {
+ string_value: "A"
+ }
+ values {
+ string_value: "7"
+ }
+ }
+ }
+ start: "A\206\310\002\234\231A\000x\234\221\010"
+ limit: "A\206\310\002\234\231A\000x\234\221\017"
+ }
+}
+
+test_case {
+ name: "GlobalIndex"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ index_name: "I"
+ part {
+ tag: 1
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: INT64
+ }
+ identifier: "k2"
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: STRING
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "8"
+ }
+ }
+ start: "\002\002\234\221\020"
+ limit: "\002\002\234\221\021"
+ }
+ test {
+ key {
+ values {
+ null_value: NULL_VALUE
+ }
+ }
+ start: "\002\002\233\000"
+ limit: "\002\002\233\001"
+ }
+}
+
+test_case {
+ name: "LocalIndex"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ index_name: "I"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: STRING
+ }
+ identifier: "k"
+ }
+ part {
+ tag: 3
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: INT64
+ }
+ identifier: "k3"
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: INT64
+ }
+ identifier: "k2"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "foo"
+ }
+ values {
+ string_value: "8"
+ }
+ }
+ start: "A\206\310\002\234\231foo\000x\006\234\221\020"
+ limit: "A\206\310\002\234\231foo\000x\006\234\221\021"
+ }
+ test {
+ key {
+ values {
+ string_value: "foo"
+ }
+ values {
+ null_value: NULL_VALUE
+ }
+ }
+ start: "A\206\310\002\234\231foo\000x\006\233\000"
+ limit: "A\206\310\002\234\231foo\000x\006\233\001"
+ }
+}
+
+test_case {
+ name: "KeySet"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "KeySet"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: INT64
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key_set {
+ keys {
+ values {
+ string_value: "99"
+ }
+ }
+ }
+ start: "A\206\310\002\234\221\306"
+ }
+ test {
+ key_set {
+ ranges {
+ start_closed {
+ values {
+ string_value: "1"
+ }
+ }
+ end_open {
+ values {
+ string_value: "10"
+ }
+ }
+ }
+ }
+ start: "A\206\310\002\234\221\002"
+ limit: "A\206\310\002\234\221\024"
+ }
+ test {
+ key_set {
+ keys {
+ values {
+ string_value: "99"
+ }
+ }
+ keys {
+ values {
+ string_value: "101"
+ }
+ }
+ }
+ start: "A\206\310\002\234\221\306"
+ limit: "A\206\310\002\234\221\313"
+ }
+ test {
+ key_set {
+ ranges {
+ start_closed {
+ values {
+ string_value: "1"
+ }
+ }
+ end_open {
+ values {
+ string_value: "10"
+ }
+ }
+ }
+ ranges {
+ start_closed {
+ values {
+ string_value: "20"
+ }
+ }
+ end_open {
+ values {
+ string_value: "30"
+ }
+ }
+ }
+ }
+ start: "A\206\310\002\234\221\002"
+ limit: "A\206\310\002\234\221<"
+ }
+ test {
+ key_set {
+ keys {
+ values {
+ string_value: "1"
+ }
+ }
+ ranges {
+ start_closed {
+ values {
+ string_value: "5"
+ }
+ }
+ end_open {
+ values {
+ string_value: "10"
+ }
+ }
+ }
+ }
+ start: "A\206\310\002\234\221\002"
+ limit: "A\206\310\002\234\221\024"
+ }
+ test {
+ key_set {
+ keys {
+ values {
+ string_value: "10"
+ }
+ }
+ ranges {
+ start_closed {
+ values {
+ string_value: "5"
+ }
+ }
+ end_open {
+ values {
+ string_value: "10"
+ }
+ }
+ }
+ }
+ start: "A\206\310\002\234\221\n"
+ limit: "A\206\310\002\234\221\025"
+ }
+}
+
+test_case {
+ name: "KeySet_All"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "T"
+ part {
+ tag: 50020
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: STRING
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key_set {
+ all: true
+ }
+ start: "A\206\310"
+ limit: "A\206\311"
+ }
+}
+
+test_case {
+ name: "InvalidRecipe_EmptyPart"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "BadRecipe"
+ part {
+ tag: 50020
+ }
+ part {
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: STRING
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "A"
+ }
+ }
+ start: "A\206\310"
+ limit: "A\206\311"
+ approximate: true
+ }
+}
+
+test_case {
+ name: "InvalidRecipe_BadOrder"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "BadRecipe"
+ part {
+ tag: 50020
+ }
+ part {
+ order: 99
+ null_order: NULLS_FIRST
+ type {
+ code: STRING
+ }
+ identifier: "k1"
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: STRING
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "A"
+ }
+ }
+ start: "A\206\310"
+ limit: "A\206\311"
+ approximate: true
+ }
+}
+
+test_case {
+ name: "InvalidRecipe_BadNullOrder"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "BadRecipe"
+ part {
+ tag: 50020
+ }
+ part {
+ order: ASCENDING
+ null_order: 99
+ type {
+ code: STRING
+ }
+ identifier: "k1"
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: STRING
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "A"
+ }
+ }
+ start: "A\206\310"
+ limit: "A\206\311"
+ approximate: true
+ }
+}
+
+test_case {
+ name: "InvalidRecipe_BadType"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "BadRecipe"
+ part {
+ tag: 50020
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: TOKENLIST
+ }
+ identifier: "k1"
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: STRING
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ key {
+ values {
+ string_value: "A"
+ }
+ }
+ start: "A\206\310"
+ limit: "A\206\311"
+ approximate: true
+ }
+}
+
+test_case {
+ name: "SimpleMutations"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "SimpleMutations"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: INT64
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ mutation {
+ insert {
+ table: "SimpleMutations"
+ columns: "k"
+ values {
+ values {
+ string_value: "80"
+ }
+ }
+ }
+ }
+ start: "A\206\310\002\234\221\240"
+ limit: "A\206\310\002\234\221\241"
+ }
+ test {
+ mutation {
+ update {
+ table: "SimpleMutations"
+ columns: "k"
+ values {
+ values {
+ string_value: "80"
+ }
+ }
+ }
+ }
+ start: "A\206\310\002\234\221\240"
+ limit: "A\206\310\002\234\221\241"
+ }
+ test {
+ mutation {
+ insert_or_update {
+ table: "SimpleMutations"
+ columns: "k"
+ values {
+ values {
+ string_value: "80"
+ }
+ }
+ }
+ }
+ start: "A\206\310\002\234\221\240"
+ limit: "A\206\310\002\234\221\241"
+ }
+ test {
+ mutation {
+ replace {
+ table: "SimpleMutations"
+ columns: "k"
+ values {
+ values {
+ string_value: "80"
+ }
+ }
+ }
+ }
+ start: "A\206\310\002\234\221\240"
+ limit: "A\206\310\002\234\221\241"
+ }
+ test {
+ mutation {
+ delete {
+ table: "SimpleMutations"
+ key_set {
+ keys {
+ values {
+ string_value: "80"
+ }
+ }
+ }
+ }
+ }
+ start: "A\206\310\002\234\221\240"
+ limit: "A\206\310\002\234\221\241"
+ }
+ test {
+ mutation {
+ delete {
+ table: "SimpleMutations"
+ key_set {
+ ranges {
+ start_closed {
+ values {
+ string_value: "80"
+ }
+ }
+ end_open {
+ values {
+ string_value: "100"
+ }
+ }
+ }
+ }
+ }
+ }
+ start: "A\206\310\002\234\221\240"
+ limit: "A\206\310\002\234\221\310"
+ }
+}
+
+test_case {
+ name: "QueueMutations"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "Q"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: INT64
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ mutation {
+ send {
+ queue: "Q"
+ key {
+ values {
+ string_value: "80"
+ }
+ }
+ payload {
+ string_value: ""
+ }
+ }
+ }
+ start: "A\206\310\002\234\221\240"
+ limit: "A\206\310\002\234\221\241"
+ }
+ test {
+ mutation {
+ ack {
+ queue: "Q"
+ key {
+ values {
+ string_value: "80"
+ }
+ }
+ }
+ }
+ start: "A\206\310\002\234\221\240"
+ limit: "A\206\310\002\234\221\241"
+ }
+}
+
+test_case {
+ name: "CustomMutationCases"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ table_name: "T"
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: STRING
+ }
+ identifier: "k"
+ }
+ }
+ }
+ test {
+ mutation {
+ }
+ start: ""
+ limit: "\377"
+ approximate: true
+ }
+ test {
+ mutation {
+ delete {
+ key_set {
+ all: true
+ }
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ }
+ test {
+ mutation {
+ delete {
+ key_set {
+ keys {
+ values {
+ string_value: "123"
+ }
+ }
+ keys {
+ values {
+ string_value: "456"
+ }
+ }
+ }
+ }
+ }
+ start: "A\206\310\002\234\231123\000x"
+ limit: "A\206\310\002\234\231456\000y"
+ }
+ test {
+ mutation {
+ delete {
+ key_set {
+ ranges {
+ start_closed {
+ values {
+ string_value: "123"
+ }
+ }
+ end_open {
+ values {
+ string_value: "456"
+ }
+ }
+ }
+ ranges {
+ start_closed {
+ values {
+ string_value: "100"
+ }
+ }
+ end_open {
+ values {
+ string_value: "200"
+ }
+ }
+ }
+ ranges {
+ start_closed {
+ values {
+ string_value: "150"
+ }
+ }
+ end_open {
+ values {
+ string_value: "500"
+ }
+ }
+ }
+ }
+ }
+ }
+ start: "A\206\310\002\234\231100\000x"
+ limit: "A\206\310\002\234\231500\000x"
+ }
+ test {
+ mutation {
+ delete {
+ key_set {
+ ranges {
+ start_closed {
+ values {
+ string_value: "123"
+ }
+ }
+ end_open {
+ values {
+ string_value: "456"
+ }
+ }
+ }
+ all: true
+ }
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ }
+ test {
+ mutation {
+ delete {
+ key_set {
+ keys {
+ values {
+ string_value: "123"
+ }
+ }
+ keys {
+ values {
+ number_value: 456
+ }
+ }
+ }
+ }
+ }
+ start: "A\206\310\002"
+ limit: "A\206\310\003"
+ approximate: true
+ }
+}
+
+test_case {
+ name: "QueryEncoding"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ operation_uid: 6
+ part {
+ tag: 50020
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: STRING
+ }
+ identifier: "p1"
+ }
+ part {
+ order: ASCENDING
+ null_order: NULLS_FIRST
+ type {
+ code: STRING
+ }
+ identifier: "p0"
+ }
+ }
+ }
+ test {
+ query_params {
+ fields {
+ key: "p0"
+ value {
+ string_value: "foo"
+ }
+ }
+ fields {
+ key: "p1"
+ value {
+ string_value: "bar"
+ }
+ }
+ }
+ start: "A\206\310\002\234\231bar\000x\234\231foo\000x"
+ }
+ test {
+ query_params {
+ fields {
+ key: "p1"
+ value {
+ string_value: "bar"
+ }
+ }
+ }
+ start: "A\206\310\002\234\231bar\000x"
+ limit: "A\206\310\002\234\231bar\000y"
+ approximate: true
+ }
+}
+
+test_case {
+ name: "RandomQueryroot"
+ recipes {
+ schema_generation: "\001\001"
+ recipe {
+ operation_uid: 7
+ part {
+ tag: 50016
+ }
+ part {
+ tag: 1
+ }
+ part {
+ order: ASCENDING
+ null_order: NOT_NULL
+ type {
+ code: INT64
+ }
+ random: true
+ }
+ }
+ }
+ test {
+ query_params {
+ }
+ start: "A\206\300\002\230\327\342\351\276\316\214%$"
+ }
+}
\ No newline at end of file