From d9f6f453c2837dba537553f6ddbcb0e3a1b1caa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Wed, 5 Aug 2026 13:53:34 +0200 Subject: [PATCH] chore(spanner): implement key_recipe routing engine for basic types Add `key_recipe` location-aware routing engine (`encode_key_from_recipe` and `encode_key_from_recipe_into`) to generate binary storage specification keys (`ssformat`) from Spanner `KeyRecipe` metadata for location-aware routing. --- src/spanner/src/routing/key_recipe.rs | 980 ++++ .../src/routing/key_recipe/golden_tests.rs | 368 ++ src/spanner/src/routing/mod.rs | 1 + src/spanner/src/routing/ssformat.rs | 10 +- .../routing/testdata/recipe_test.textproto | 3943 +++++++++++++++++ 5 files changed, 5297 insertions(+), 5 deletions(-) create mode 100644 src/spanner/src/routing/key_recipe.rs create mode 100644 src/spanner/src/routing/key_recipe/golden_tests.rs create mode 100644 src/spanner/src/routing/testdata/recipe_test.textproto diff --git a/src/spanner/src/routing/key_recipe.rs b/src/spanner/src/routing/key_recipe.rs new file mode 100644 index 0000000000..6084d42a4f --- /dev/null +++ b/src/spanner/src/routing/key_recipe.rs @@ -0,0 +1,980 @@ +// 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 +// +// https://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. + +//! Key recipe evaluation engine for Spanner location-aware routing. +//! +//! Evaluates a [`KeyRecipe`] against SQL query parameter values or primary key tuples and encodes +//! them into a lexicographical binary storage specification key (`Vec`) using [`ssformat`](crate::routing::ssformat). + +// TODO(#6236): Remove dead_code allowance once KeyRecipe and KeyRecipeCache are integrated into DatabaseClient. +#![allow(dead_code)] + +use crate::Result; +use crate::model::key_recipe::Part; +use crate::model::key_recipe::part::{NullOrder, Order}; +use crate::model::{KeyRecipe, TypeCode}; +use crate::routing::ssformat; +use crate::value::{Kind, Value}; +use base64::Engine; +use base64::prelude::BASE64_STANDARD; + +/// Encodes a Spanner routing key (`Vec`) from a [`KeyRecipe`] and a slice of column [`Value`]s. +/// +/// Allocates a new vector with sufficient capacity for the encoded parts. For zero-allocation +/// key encoding into a reused scratch buffer on hot query paths, use [`encode_key_from_recipe_into`]. +/// +/// Each part of the recipe is evaluated in order: +/// - If `part.tag != 0`, the tag number is appended to the binary key. +/// - If `part.tag == 0`, the corresponding column value from `values` is encoded using `ssformat` +/// according to the part's sort order (`ASCENDING` / `DESCENDING`) and null order (`NULLS_FIRST` / `NULLS_LAST`). +/// +/// # Caller Fallback Contract +/// If encoding returns an error (for example, due to an unsupported key column type like `NUMERIC` +/// or `FLOAT32`), callers (`LocationRouter` / `DatabaseClient`) MUST catch the error and silently +/// fall back to default routing (without tablet affinity) rather than failing the user's RPC. +pub(crate) fn encode_key_from_recipe(recipe: &KeyRecipe, values: &[Value]) -> Result> { + let mut buffer = Vec::with_capacity(recipe.part.len().saturating_mul(16)); + encode_key_from_recipe_into(recipe, values, &mut buffer)?; + Ok(buffer) +} + +/// Encodes a Spanner routing key from a [`KeyRecipe`] and a slice of column [`Value`]s directly +/// into an existing output buffer. +/// +/// This avoids new `Vec` heap allocations when callers reuse a scratch buffer across RPCs +/// on the Spanner Omni hot path. +/// +/// # Caller Fallback Contract +/// If encoding returns an error (for example, due to an unsupported key column type like `NUMERIC` +/// or `FLOAT32`), callers (`LocationRouter` / `DatabaseClient`) MUST catch the error and silently +/// fall back to default routing (without tablet affinity) rather than failing the user's RPC. +pub(crate) fn encode_key_from_recipe_into( + recipe: &KeyRecipe, + values: &[Value], + buffer: &mut Vec, +) -> Result<()> { + if recipe.part.is_empty() { + return Err(crate::error::internal_error( + "Invalid KeyRecipe: must have at least one part", + )); + } + if recipe.part[0].tag == 0 { + return Err(crate::error::internal_error( + "Invalid KeyRecipe: must start with a table or index tag", + )); + } + + let initial_len = buffer.len(); + let mut values_iter = values.iter(); + + for part in &recipe.part { + if part.tag != 0 { + if let Err(e) = ssformat::append_composite_tag(buffer, part.tag) { + buffer.truncate(initial_len); + return Err(e); + } + continue; + } + + let value = match values_iter.next() { + Some(v) => v, + None => { + buffer.truncate(initial_len); + return Err(crate::error::internal_error( + "Not enough column values to encode key recipe: more values required", + )); + } + }; + + if let Err(e) = encode_part(buffer, part, value) { + buffer.truncate(initial_len); + return Err(e); + } + } + + Ok(()) +} + +/// Verifies that the recipe part's data type is supported by this key recipe encoder. +/// +/// According to [Spanner documentation](https://docs.cloud.google.com/spanner/docs/reference/standard-sql/data-types#valid_key_column_types), +/// all data types are valid key column types except for `FLOAT32`, `ARRAY`, `JSON`, and `STRUCT`. +/// +/// Currently, this encoder supports types whose wire-format encoding preserves lexicographical +/// sort order without custom binary date/timestamp packing: `BOOL`, `INT64`, `FLOAT64`, `STRING`, +/// and `BYTES`. +/// +/// Note: `NUMERIC`, `DATE`, and `TIMESTAMP` are valid Spanner key column types, but are not +/// currently supported here because they require specialized binary storage encodings (`ssformat`) +/// (e.g., days since epoch for `DATE`, 12-byte binary format for `TIMESTAMP`, decimal bit-packing +/// for `NUMERIC`). Unsupported types explicitly return an error so that requests gracefully fall back +/// to default routing rather than emitting invalid shard keys. +fn check_supported_key_type(part: &Part) -> Result<&TypeCode> { + let part_type = part.r#type.as_ref().ok_or_else(|| { + crate::error::internal_error("Invalid KeyRecipe part: missing type definition") + })?; + match &part_type.code { + TypeCode::Bool + | TypeCode::Int64 + | TypeCode::Float64 + | TypeCode::String + | TypeCode::Bytes => Ok(&part_type.code), + TypeCode::Unspecified => Err(crate::error::internal_error( + "Invalid KeyRecipe part: TypeCode::Unspecified is not permitted", + )), + // TODO(#6279): Support TypeCode::Date binary ssformat encoding in a separate pull request. + TypeCode::Date => Err(crate::error::internal_error( + "TypeCode::Date is not yet supported for key recipe encoding (TODO(#6279): will be added in a separate pull request)", + )), + // TODO(#6279): Support TypeCode::Timestamp binary ssformat encoding in a separate pull request. + TypeCode::Timestamp => Err(crate::error::internal_error( + "TypeCode::Timestamp is not yet supported for key recipe encoding (TODO(#6279): will be added in a separate pull request)", + )), + // TODO(#6279): Support TypeCode::Uuid binary ssformat encoding in a separate pull request. + TypeCode::Uuid => Err(crate::error::internal_error( + "TypeCode::Uuid is not yet supported for key recipe encoding (TODO(#6279): will be added in a separate pull request)", + )), + // TODO(#6279): Support TypeCode::Enum binary ssformat encoding in a separate pull request. + TypeCode::Enum => Err(crate::error::internal_error( + "TypeCode::Enum is not yet supported for key recipe encoding (TODO(#6279): will be added in a separate pull request)", + )), + unsupported => Err(crate::error::internal_error(format!( + "TypeCode {unsupported:?} is not supported for key recipe encoding", + ))), + } +} + +/// Evaluates a single key column part against a [`Value`] and appends it to `buffer`. +fn encode_part(buffer: &mut Vec, part: &Part, value: &Value) -> Result<()> { + let type_code = check_supported_key_type(part)?; + + let decreasing = matches!(part.order, Order::Descending); + + if value.kind() == Kind::Null { + return encode_null_part(buffer, &part.null_order, decreasing); + } + + // Emit NOT NULL prefix marker byte before encoding any non-null value on nullable columns. + encode_not_null_marker(buffer, &part.null_order, decreasing)?; + + match *type_code { + TypeCode::Bool => encode_bool_part(buffer, value, decreasing), + TypeCode::Int64 => encode_int64_part(buffer, value, decreasing), + TypeCode::Float64 => encode_float64_part(buffer, value, decreasing), + TypeCode::String => encode_string_part(buffer, value, decreasing), + TypeCode::Bytes => encode_bytes_part(buffer, value, decreasing), + ref other => Err(crate::error::internal_error(format!( + "Unsupported TypeCode {other:?} for key recipe encoding", + ))), + } +} + +/// Appends a NULL value marker according to [`NullOrder`]. +fn encode_null_part(buffer: &mut Vec, null_order: &NullOrder, decreasing: bool) -> Result<()> { + match null_order { + NullOrder::NullsFirst | NullOrder::UnknownValue(_) => append_null_ordered(buffer, false), + NullOrder::NullsLast => append_null_ordered(buffer, true), + NullOrder::Unspecified => append_null_ordered(buffer, decreasing), + NullOrder::NotNull => Err(crate::error::internal_error( + "NULL value provided for NOT NULL key recipe column", + )), + } +} + +/// Appends a NOT NULL prefix marker byte before non-null values on nullable key columns. +fn encode_not_null_marker( + buffer: &mut Vec, + null_order: &NullOrder, + decreasing: bool, +) -> Result<()> { + match null_order { + NullOrder::NullsFirst | NullOrder::UnknownValue(_) => { + append_not_null_marker_ordered(buffer, false) + } + NullOrder::NullsLast => append_not_null_marker_ordered(buffer, true), + NullOrder::Unspecified => append_not_null_marker_ordered(buffer, decreasing), + NullOrder::NotNull => { + // NOT NULL columns do not emit nullable marker bytes. + Ok(()) + } + } +} + +/// Evaluates a boolean column value (`BOOL`). +fn encode_bool_part(buffer: &mut Vec, value: &Value, decreasing: bool) -> Result<()> { + let boolean_value = value.try_as_bool().ok_or_else(|| { + crate::error::internal_error("Type mismatch: expected Bool value for BOOL column") + })?; + append_bool_ordered(buffer, boolean_value, decreasing) +} + +/// Evaluates an integer column value (`INT64`). +fn encode_int64_part(buffer: &mut Vec, value: &Value, decreasing: bool) -> Result<()> { + let string_value = value.try_as_string().ok_or_else(|| { + crate::error::internal_error("Type mismatch: expected String value for INT64 column") + })?; + let integer_value = string_value.parse::().map_err(|e| { + crate::error::internal_error(format!( + "Failed to parse Int64 from string '{string_value}': {e}" + )) + })?; + append_int64_ordered(buffer, integer_value, decreasing) +} + +/// Evaluates a floating-point column value (`FLOAT64`). +fn encode_float64_part(buffer: &mut Vec, value: &Value, decreasing: bool) -> Result<()> { + if let Some(string_value) = value.try_as_string() { + let num = match string_value { + "NaN" => f64::NAN, + "Infinity" => f64::INFINITY, + "-Infinity" => f64::NEG_INFINITY, + _ => { + return Err(crate::error::internal_error(format!( + "Type mismatch: invalid FLOAT64 string '{string_value}' (only NaN and Infinity strings permitted)", + ))); + } + }; + return append_double_ordered(buffer, num, decreasing); + } + let num = value.try_as_f64().ok_or_else(|| { + crate::error::internal_error( + "Type mismatch: expected Number or special String value for FLOAT64 column", + ) + })?; + append_double_ordered(buffer, num, decreasing) +} + +/// Evaluates a string column value (`STRING`). +fn encode_string_part(buffer: &mut Vec, value: &Value, decreasing: bool) -> Result<()> { + let string_value = value.try_as_string().ok_or_else(|| { + crate::error::internal_error("Type mismatch: expected String value for STRING column") + })?; + append_string_ordered(buffer, string_value, decreasing) +} + +/// Evaluates a byte array column value (`BYTES`). +fn encode_bytes_part(buffer: &mut Vec, value: &Value, decreasing: bool) -> Result<()> { + let string_value = value.try_as_string().ok_or_else(|| { + crate::error::internal_error("Type mismatch: expected base64 String value for BYTES column") + })?; + let mut stack_buffer = [0u8; 512]; + if let Ok(len) = BASE64_STANDARD.decode_slice(string_value, &mut stack_buffer) { + return append_bytes_ordered(buffer, &stack_buffer[..len], decreasing); + } + let bytes_value = BASE64_STANDARD.decode(string_value).map_err(|e| { + crate::error::internal_error(format!( + "Failed to decode base64 Bytes from string '{string_value}': {e}" + )) + })?; + append_bytes_ordered(buffer, &bytes_value, decreasing) +} + +#[inline] +fn append_int64_ordered(buffer: &mut Vec, value: i64, decreasing: bool) -> Result<()> { + if decreasing { + ssformat::append_int64_decreasing(buffer, value); + } else { + ssformat::append_int64_increasing(buffer, value); + } + Ok(()) +} + +#[inline] +fn append_bytes_ordered(buffer: &mut Vec, value: &[u8], decreasing: bool) -> Result<()> { + if decreasing { + ssformat::append_bytes_decreasing(buffer, value); + } else { + ssformat::append_bytes_increasing(buffer, value); + } + Ok(()) +} + +#[inline] +fn append_string_ordered(buffer: &mut Vec, value: &str, decreasing: bool) -> Result<()> { + if decreasing { + ssformat::append_string_decreasing(buffer, value); + } else { + ssformat::append_string_increasing(buffer, value); + } + Ok(()) +} + +#[inline] +fn append_double_ordered(buffer: &mut Vec, value: f64, decreasing: bool) -> Result<()> { + if decreasing { + ssformat::append_double_decreasing(buffer, value); + } else { + ssformat::append_double_increasing(buffer, value); + } + Ok(()) +} + +#[inline] +fn append_bool_ordered(buffer: &mut Vec, value: bool, decreasing: bool) -> Result<()> { + if decreasing { + ssformat::append_bool_decreasing(buffer, value); + } else { + ssformat::append_bool_increasing(buffer, value); + } + Ok(()) +} + +#[inline] +fn append_null_ordered(buffer: &mut Vec, nulls_last: bool) -> Result<()> { + if nulls_last { + ssformat::append_null_ordered_last(buffer); + } else { + ssformat::append_null_ordered_first(buffer); + } + Ok(()) +} + +#[inline] +fn append_not_null_marker_ordered(buffer: &mut Vec, nulls_last: bool) -> Result<()> { + if nulls_last { + ssformat::append_not_null_marker_null_ordered_last(buffer); + } else { + ssformat::append_not_null_marker_null_ordered_first(buffer); + } + Ok(()) +} + +#[cfg(test)] +mod golden_tests; + +#[cfg(test)] +mod tests { + use super::*; + use crate::model::Type; + use crate::model::key_recipe::Part; + use crate::value::ToValue; + use prost_types::{ListValue, Value as ProstValue, value::Kind as ProstValueKind}; + + fn sample_recipe(parts: Vec) -> KeyRecipe { + let mut all_parts = vec![Part::new().set_tag(1u32)]; + all_parts.extend(parts); + KeyRecipe::new().set_part(all_parts) + } + + fn string_part(order: Order) -> Part { + Part::new() + .set_order(order) + .set_type(Type::default().set_code(TypeCode::String)) + } + + #[test] + fn encode_key_from_recipe_invalid_recipe_returns_err() { + let empty_recipe = KeyRecipe::new(); + let error = encode_key_from_recipe(&empty_recipe, &[]) + .expect_err("empty recipe should return error"); + assert!( + error.to_string().contains("must have at least one part"), + "unexpected error message: {error}" + ); + + let no_tag_recipe = KeyRecipe::new().set_part(vec![string_part(Order::Ascending)]); + let error2 = encode_key_from_recipe(&no_tag_recipe, &["alpha".to_value()]) + .expect_err("recipe without leading tag should return error"); + assert!( + error2 + .to_string() + .contains("must start with a table or index tag"), + "unexpected error message: {error2}" + ); + } + + #[test] + fn encode_key_from_recipe_composite_tags_only() { + let recipe = KeyRecipe::new().set_part(vec![Part::new().set_tag(1u32)]); + let encoded = encode_key_from_recipe(&recipe, &[]) + .expect("tag encoding should succeed without column values"); + assert!( + !encoded.is_empty(), + "encoded tag buffer should not be empty" + ); + } + + #[test] + fn encode_key_from_recipe_composite_tag_overflow_returns_err() { + let recipe = KeyRecipe::new().set_part(vec![Part::new().set_tag(u32::MAX)]); + let error = encode_key_from_recipe(&recipe, &[]) + .expect_err("exceeding K_MAX_FIELD_TAG should return error"); + assert!( + error.to_string().contains("Invalid tag value"), + "unexpected error message: {error}" + ); + } + + #[test] + fn encode_key_from_recipe_string_ascending_and_descending() { + let asc_recipe = sample_recipe(vec![string_part(Order::Ascending)]); + let desc_recipe = sample_recipe(vec![string_part(Order::Descending)]); + let values = vec!["alpha".to_value()]; + + let asc_encoded = encode_key_from_recipe(&asc_recipe, &values) + .expect("ascending string encoding should succeed"); + let desc_encoded = encode_key_from_recipe(&desc_recipe, &values) + .expect("descending string encoding should succeed"); + + assert_ne!( + asc_encoded, desc_encoded, + "ascending and descending encodings must differ" + ); + } + + #[test] + fn encode_key_from_recipe_int64_string_encoding() { + let part = Part::new() + .set_order(Order::Ascending) + .set_type(Type::default().set_code(TypeCode::Int64)); + + let recipe = sample_recipe(vec![part]); + let values = vec!["12345".to_value()]; + + let encoded = + encode_key_from_recipe(&recipe, &values).expect("int64 string encoding should succeed"); + assert!(!encoded.is_empty(), "encoded buffer should not be empty"); + } + + #[test] + fn encode_key_from_recipe_float_number_encoding() { + let asc_recipe = sample_recipe(vec![ + Part::new() + .set_order(Order::Ascending) + .set_type(Type::default().set_code(TypeCode::Float64)), + ]); + let desc_recipe = sample_recipe(vec![ + Part::new() + .set_order(Order::Descending) + .set_type(Type::default().set_code(TypeCode::Float64)), + ]); + let values = vec![2.5_f64.to_value()]; + + let asc_encoded = encode_key_from_recipe(&asc_recipe, &values) + .expect("ascending float encoding should succeed"); + let desc_encoded = encode_key_from_recipe(&desc_recipe, &values) + .expect("descending float encoding should succeed"); + assert_ne!( + asc_encoded, desc_encoded, + "ascending and descending float encodings must differ" + ); + } + + #[test] + fn encode_key_from_recipe_float_string_nan_infinity_encoding() { + let recipe = sample_recipe(vec![ + Part::new() + .set_order(Order::Ascending) + .set_type(Type::default().set_code(TypeCode::Float64)), + ]); + + let nan_encoded = encode_key_from_recipe(&recipe, &["NaN".to_value()]) + .expect("NaN string float encoding should succeed"); + let inf_encoded = encode_key_from_recipe(&recipe, &["Infinity".to_value()]) + .expect("Infinity string float encoding should succeed"); + let neg_inf_encoded = encode_key_from_recipe(&recipe, &["-Infinity".to_value()]) + .expect("-Infinity string float encoding should succeed"); + + assert_ne!( + nan_encoded, inf_encoded, + "NaN and Infinity must encode differently" + ); + assert_ne!( + inf_encoded, neg_inf_encoded, + "Infinity and -Infinity must encode differently" + ); + } + + #[test] + fn encode_key_from_recipe_bytes_base64_fast_and_slow_paths() { + let part = Part::new() + .set_order(Order::Ascending) + .set_type(Type::default().set_code(TypeCode::Bytes)); + let recipe = sample_recipe(vec![part]); + + // 1. Fast path (<= 512 bytes: stack-allocated buffer decoding) + let fast_base64 = BASE64_STANDARD.encode(b"hello spanner fast path"); + let fast_encoded = encode_key_from_recipe(&recipe, &[fast_base64.to_value()]) + .expect("fast path base64 bytes encoding should succeed"); + assert!( + !fast_encoded.is_empty(), + "fast path encoded buffer should not be empty" + ); + + // 2. Slow path (> 512 bytes: heap fallback decoding) + let large_payload = vec![0xAB_u8; 1024]; + let slow_base64 = BASE64_STANDARD.encode(&large_payload); + let slow_encoded = encode_key_from_recipe(&recipe, &[slow_base64.to_value()]) + .expect("slow path base64 bytes encoding should succeed"); + assert!( + slow_encoded.len() > fast_encoded.len(), + "slow path encoded buffer should be larger" + ); + + // 3. Error path (invalid base64 string should fail cleanly) + let invalid_err = encode_key_from_recipe(&recipe, &["!!!not-base64!!!".to_value()]) + .expect_err("invalid base64 string should return error"); + assert!( + invalid_err + .to_string() + .contains("Failed to decode base64 Bytes"), + "unexpected error message: {invalid_err}" + ); + } + + #[test] + fn encode_key_from_recipe_strict_type_validation_returns_err() { + // 1. INT64 column must reject non-string values (e.g. Bool or Number) + let int64_recipe = sample_recipe(vec![ + Part::new() + .set_order(Order::Ascending) + .set_type(Type::default().set_code(TypeCode::Int64)), + ]); + let err1 = encode_key_from_recipe(&int64_recipe, &[true.to_value()]) + .expect_err("Bool value for INT64 column should return error"); + assert!( + err1.to_string() + .contains("Type mismatch: expected String value for INT64 column"), + "unexpected error message: {err1}" + ); + + // 2. FLOAT64 column must reject regular number strings ("123.45") + let float_recipe = sample_recipe(vec![ + Part::new() + .set_order(Order::Ascending) + .set_type(Type::default().set_code(TypeCode::Float64)), + ]); + let err2 = encode_key_from_recipe(&float_recipe, &["123.45".to_value()]) + .expect_err("regular number string for FLOAT64 column should return error"); + assert!( + err2.to_string() + .contains("only NaN and Infinity strings permitted"), + "unexpected error message: {err2}" + ); + + // 3. BOOL column must reject non-bool values + let bool_recipe = sample_recipe(vec![ + Part::new() + .set_order(Order::Ascending) + .set_type(Type::default().set_code(TypeCode::Bool)), + ]); + let err3 = encode_key_from_recipe(&bool_recipe, &["true".to_value()]) + .expect_err("String value for BOOL column should return error"); + assert!( + err3.to_string() + .contains("Type mismatch: expected Bool value for BOOL column"), + "unexpected error message: {err3}" + ); + } + + #[test] + fn encode_key_from_recipe_unspecified_or_missing_type_returns_err() { + // 1. TypeCode::Unspecified must be rejected + let unspecified_recipe = sample_recipe(vec![ + Part::new() + .set_order(Order::Ascending) + .set_type(Type::default().set_code(TypeCode::Unspecified)), + ]); + let err1 = encode_key_from_recipe(&unspecified_recipe, &["alpha".to_value()]) + .expect_err("TypeCode::Unspecified should return error"); + assert!( + err1.to_string() + .contains("TypeCode::Unspecified is not permitted"), + "unexpected error message: {err1}" + ); + + // 2. Missing type definition (None) must be rejected + let missing_type_recipe = sample_recipe(vec![Part::new().set_order(Order::Ascending)]); + let err2 = encode_key_from_recipe(&missing_type_recipe, &["alpha".to_value()]) + .expect_err("missing type definition should return error"); + assert!( + err2.to_string().contains("missing type definition"), + "unexpected error message: {err2}" + ); + } + + #[test] + fn encode_key_from_recipe_bool_encoding() { + let asc_recipe = sample_recipe(vec![ + Part::new() + .set_order(Order::Ascending) + .set_type(Type::default().set_code(TypeCode::Bool)), + ]); + let desc_recipe = sample_recipe(vec![ + Part::new() + .set_order(Order::Descending) + .set_type(Type::default().set_code(TypeCode::Bool)), + ]); + let values = vec![true.to_value()]; + + let asc_encoded = encode_key_from_recipe(&asc_recipe, &values) + .expect("ascending bool encoding should succeed"); + let desc_encoded = encode_key_from_recipe(&desc_recipe, &values) + .expect("descending bool encoding should succeed"); + assert_ne!( + asc_encoded, desc_encoded, + "ascending and descending bool encodings must differ" + ); + } + + #[test] + fn encode_key_from_recipe_null_ordering() { + let first_recipe = sample_recipe(vec![ + Part::new() + .set_null_order(NullOrder::NullsFirst) + .set_type(Type::default().set_code(TypeCode::String)), + ]); + let last_recipe = sample_recipe(vec![ + Part::new() + .set_null_order(NullOrder::NullsLast) + .set_type(Type::default().set_code(TypeCode::String)), + ]); + let values = vec![Value::null()]; + + let first_encoded = encode_key_from_recipe(&first_recipe, &values) + .expect("nulls first encoding should succeed"); + let last_encoded = encode_key_from_recipe(&last_recipe, &values) + .expect("nulls last encoding should succeed"); + assert_ne!( + first_encoded, last_encoded, + "nulls first and last encodings must differ" + ); + } + + #[test] + fn encode_key_from_recipe_not_enough_values_returns_err() { + let recipe = sample_recipe(vec![string_part(Order::Ascending)]); + let error = encode_key_from_recipe(&recipe, &[]) + .expect_err("missing column values should return error"); + assert!( + error.to_string().contains("Not enough column values"), + "unexpected error message: {error}" + ); + } + + #[test] + fn encode_key_from_recipe_not_null_violation_returns_err() { + let recipe = sample_recipe(vec![ + Part::new() + .set_null_order(NullOrder::NotNull) + .set_type(Type::default().set_code(TypeCode::String)), + ]); + let values = vec![Value::null()]; + let error = encode_key_from_recipe(&recipe, &values) + .expect_err("null value for NOT NULL column should return error"); + assert!( + error + .to_string() + .contains("NULL value provided for NOT NULL"), + "unexpected error message: {error}" + ); + } + + #[test] + fn encode_key_from_recipe_unsupported_value_kind_returns_err() { + let recipe = sample_recipe(vec![string_part(Order::Ascending)]); + let values = vec![Value(ProstValue { + kind: Some(ProstValueKind::ListValue(ListValue::default())), + })]; + let error = encode_key_from_recipe(&recipe, &values) + .expect_err("unsupported value kind should return error"); + assert!( + error + .to_string() + .contains("Type mismatch: expected String value"), + "unexpected error message: {error}" + ); + } + + #[test] + fn encode_key_from_recipe_unsupported_type_float32_returns_err() { + let recipe = sample_recipe(vec![ + Part::new() + .set_order(Order::Ascending) + .set_type(Type::default().set_code(TypeCode::Float32)), + ]); + let values = vec!["1.0".to_value()]; + let error = + encode_key_from_recipe(&recipe, &values).expect_err("Float32 should return error"); + assert!( + error + .to_string() + .contains("is not supported for key recipe encoding"), + "unexpected error message: {error}" + ); + } + + #[test] + fn encode_key_from_recipe_unsupported_type_numeric_returns_err() { + let recipe = sample_recipe(vec![ + Part::new() + .set_order(Order::Ascending) + .set_type(Type::default().set_code(TypeCode::Numeric)), + ]); + let values = vec!["100.5".to_value()]; + let error = + encode_key_from_recipe(&recipe, &values).expect_err("Numeric should return error"); + assert!( + error + .to_string() + .contains("is not supported for key recipe encoding"), + "unexpected error message: {error}" + ); + } + + #[test] + fn encode_key_from_recipe_unsupported_type_timestamp_returns_err() { + let recipe = sample_recipe(vec![ + Part::new() + .set_order(Order::Ascending) + .set_type(Type::default().set_code(TypeCode::Timestamp)), + ]); + let values = vec!["2026-08-05T00:00:00Z".to_value()]; + let error = encode_key_from_recipe(&recipe, &values) + .expect_err("Timestamp should return error until binary ssformat is implemented"); + assert!( + error + .to_string() + .contains("is not yet supported for key recipe encoding"), + "unexpected error message: {error}" + ); + } + + #[test] + fn encode_key_from_recipe_unsupported_type_date_returns_err() { + let recipe = sample_recipe(vec![ + Part::new() + .set_order(Order::Ascending) + .set_type(Type::default().set_code(TypeCode::Date)), + ]); + let values = vec!["2026-08-05".to_value()]; + let error = encode_key_from_recipe(&recipe, &values) + .expect_err("Date should return error until binary ssformat is implemented"); + assert!( + error + .to_string() + .contains("is not yet supported for key recipe encoding"), + "unexpected error message: {error}" + ); + } + + #[test] + fn encode_key_from_recipe_unspecified_null_order_defaults_correctly() { + let asc_recipe = sample_recipe(vec![ + Part::new() + .set_order(Order::Ascending) + .set_null_order(NullOrder::Unspecified) + .set_type(Type::default().set_code(TypeCode::String)), + ]); + let desc_recipe = sample_recipe(vec![ + Part::new() + .set_order(Order::Descending) + .set_null_order(NullOrder::Unspecified) + .set_type(Type::default().set_code(TypeCode::String)), + ]); + let values = vec![Value::null()]; + + let asc_encoded = + encode_key_from_recipe(&asc_recipe, &values).expect("asc null encoding should succeed"); + let desc_encoded = encode_key_from_recipe(&desc_recipe, &values) + .expect("desc null encoding should succeed"); + assert_ne!( + asc_encoded, desc_encoded, + "ascending and descending unspecified null orders must differ" + ); + } + + #[test] + fn encode_key_from_recipe_into_scratch_buffer() { + let recipe = sample_recipe(vec![string_part(Order::Ascending)]); + let values = vec!["spanner".to_value()]; + let mut buffer = Vec::with_capacity(32); + + encode_key_from_recipe_into(&recipe, &values, &mut buffer) + .expect("encoding into reusable buffer should succeed"); + assert!( + !buffer.is_empty(), + "encoded scratch buffer should not be empty" + ); + + let direct_encoded = + encode_key_from_recipe(&recipe, &values).expect("direct encoding should succeed"); + assert_eq!( + buffer, direct_encoded, + "reusable buffer output must match direct encoding" + ); + } + + #[test] + fn encode_key_from_recipe_unsupported_type_uuid_returns_err() { + let recipe = sample_recipe(vec![ + Part::new() + .set_order(Order::Ascending) + .set_type(Type::default().set_code(TypeCode::Uuid)), + ]); + let values = vec!["123e4567-e89b-12d3-a456-426614174000".to_value()]; + let error = encode_key_from_recipe(&recipe, &values) + .expect_err("Uuid should return error until binary ssformat is implemented"); + assert!( + error + .to_string() + .contains("is not yet supported for key recipe encoding"), + "unexpected error message: {error}" + ); + } + + #[test] + fn encode_key_from_recipe_unsupported_type_enum_returns_err() { + let recipe = sample_recipe(vec![ + Part::new() + .set_order(Order::Ascending) + .set_type(Type::default().set_code(TypeCode::Enum)), + ]); + let values = vec!["VALUE_1".to_value()]; + let error = encode_key_from_recipe(&recipe, &values) + .expect_err("Enum should return error until binary ssformat is implemented"); + assert!( + error + .to_string() + .contains("is not yet supported for key recipe encoding"), + "unexpected error message: {error}" + ); + } + + #[test] + fn encode_key_from_recipe_unsupported_type_code_other_returns_err() { + let recipe = sample_recipe(vec![ + Part::new() + .set_order(Order::Ascending) + .set_type(Type::default().set_code(TypeCode::Array)), + ]); + let values = vec!["[]".to_value()]; + let error = encode_key_from_recipe(&recipe, &values) + .expect_err("Array should return error as unsupported type"); + assert!( + error + .to_string() + .contains("is not supported for key recipe encoding"), + "unexpected error message: {error}" + ); + } + + #[test] + fn encode_key_from_recipe_invalid_int64_string_returns_err() { + let recipe = sample_recipe(vec![ + Part::new() + .set_order(Order::Ascending) + .set_type(Type::default().set_code(TypeCode::Int64)), + ]); + let values = vec!["not_an_int".to_value()]; + let error = encode_key_from_recipe(&recipe, &values) + .expect_err("non-numeric int64 string should return error"); + assert!( + error + .to_string() + .contains("Failed to parse Int64 from string"), + "unexpected error message: {error}" + ); + } + + #[test] + fn encode_key_from_recipe_unknown_null_order() { + let recipe_null = sample_recipe(vec![ + Part::new() + .set_null_order(NullOrder::from(99)) + .set_type(Type::default().set_code(TypeCode::String)), + ]); + let null_encoded = encode_key_from_recipe(&recipe_null, &[Value::null()]) + .expect("UnknownValue null order should succeed for null"); + assert!( + !null_encoded.is_empty(), + "null encoded buffer should not be empty" + ); + + let value_encoded = encode_key_from_recipe(&recipe_null, &["spanner".to_value()]) + .expect("UnknownValue null order should succeed for non-null value"); + assert!( + value_encoded.len() > null_encoded.len(), + "non-null value should include prefix marker byte and content" + ); + } + + #[test] + fn encode_key_from_recipe_type_mismatches_all_columns_returns_err() { + // 1. STRING column rejects bool value + let string_recipe = sample_recipe(vec![string_part(Order::Ascending)]); + let err1 = encode_key_from_recipe(&string_recipe, &[true.to_value()]) + .expect_err("Bool value for STRING column should fail"); + assert!( + err1.to_string() + .contains("Type mismatch: expected String value for STRING column"), + "unexpected error message: {err1}" + ); + + // 2. BYTES column rejects bool value + let bytes_recipe = sample_recipe(vec![ + Part::new() + .set_order(Order::Ascending) + .set_type(Type::default().set_code(TypeCode::Bytes)), + ]); + let err2 = encode_key_from_recipe(&bytes_recipe, &[true.to_value()]) + .expect_err("Bool value for BYTES column should fail"); + assert!( + err2.to_string() + .contains("Type mismatch: expected base64 String value for BYTES column"), + "unexpected error message: {err2}" + ); + + // 3. FLOAT64 column rejects bool value (neither String nor f64 number) + let float_recipe = sample_recipe(vec![ + Part::new() + .set_order(Order::Ascending) + .set_type(Type::default().set_code(TypeCode::Float64)), + ]); + let err3 = encode_key_from_recipe(&float_recipe, &[true.to_value()]) + .expect_err("Bool value for FLOAT64 column should fail"); + assert!( + err3.to_string().contains( + "Type mismatch: expected Number or special String value for FLOAT64 column" + ), + "unexpected error message: {err3}" + ); + } + + #[test] + fn encode_key_from_recipe_into_truncates_buffer_on_error() { + let recipe = sample_recipe(vec![ + string_part(Order::Ascending), + Part::new() + .set_order(Order::Ascending) + .set_type(Type::default().set_code(TypeCode::Date)), + ]); + let values = vec!["spanner".to_value(), "2026-08-05".to_value()]; + let mut buffer = b"existing_prefix".to_vec(); + let initial_len = buffer.len(); + + let err = encode_key_from_recipe_into(&recipe, &values, &mut buffer) + .expect_err("second column Date should fail"); + assert_eq!( + buffer.len(), + initial_len, + "buffer must be truncated back to its initial length on error: {err}" + ); + assert_eq!( + &buffer[..], + b"existing_prefix", + "existing buffer contents must remain untouched" + ); + } +} diff --git a/src/spanner/src/routing/key_recipe/golden_tests.rs b/src/spanner/src/routing/key_recipe/golden_tests.rs new file mode 100644 index 0000000000..3522221988 --- /dev/null +++ b/src/spanner/src/routing/key_recipe/golden_tests.rs @@ -0,0 +1,368 @@ +// 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 +// +// https://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. + +use super::*; +use crate::model::key_recipe::Part; +use crate::model::key_recipe::part::{NullOrder, Order}; +use crate::model::{KeyRecipe, Type, TypeCode}; +use crate::value::{ToValue, Value}; +use std::fs; +use std::iter::Peekable; +use std::path::Path; + +/// Unescapes C-style octal escape sequences (e.g. `\206`, `\310`, `\002`) and standard ASCII escapes +/// from Protobuf `textproto` byte strings. +fn unescape_bytes(s: &str) -> Vec { + let mut out = Vec::with_capacity(s.len()); + let mut bytes = s.bytes().peekable(); + + while let Some(b) = bytes.next() { + if b != b'\\' { + out.push(b); + continue; + } + + // Try to parse up to 3 octal digits (`\ooo`) which represent raw byte values. + if let Some(octal_byte) = try_parse_octal_escape(&mut bytes) { + out.push(octal_byte); + continue; + } + + // Otherwise, handle standard ASCII escape sequences (`\n`, `\r`, `\t`, etc.). + if let Some(next_b) = bytes.next() { + let escaped = match next_b { + b'n' => b'\n', + b'r' => b'\r', + b't' => b'\t', + b'\\' => b'\\', + b'"' => b'"', + _ => next_b, + }; + out.push(escaped); + } + } + + out +} + +/// Helper that attempts to read up to 3 consecutive octal digits from a byte stream. +/// Returns `Some(byte)` if at least one octal digit was consumed, or `None` otherwise. +fn try_parse_octal_escape>(bytes: &mut Peekable) -> Option { + let mut value = 0u8; + let mut parsed_digits = 0; + + for _ in 0..3 { + if let Some(&b) = bytes.peek() { + if (b'0'..=b'7').contains(&b) { + let digit = b - b'0'; + value = value.wrapping_mul(8).wrapping_add(digit); + bytes.next(); // Consume the octal digit byte. + parsed_digits += 1; + } else { + break; + } + } + } + + if parsed_digits > 0 { Some(value) } else { None } +} + +struct ParsedTestCase { + name: String, + recipe: KeyRecipe, + tests: Vec, +} + +struct ParsedTest { + values: Vec, + start: Vec, + approximate: bool, +} + +/// Simple line-trimming helper that extracts the value after a specified field prefix +/// (e.g., stripping `"name: "` and removing surrounding quotes). +fn extract_value<'a>(line: &'a str, prefix: &str) -> Option<&'a str> { + line.trim() + .strip_prefix(prefix) + .map(|rest| rest.trim_matches(|c| c == ' ' || c == '"')) +} + +/// Maps Spanner data type code names in `recipe_test.textproto` to [`TypeCode`]. +fn parse_type_code(code_string: &str) -> Option { + match code_string { + "BOOL" => Some(TypeCode::Bool), + "INT64" => Some(TypeCode::Int64), + "FLOAT64" => Some(TypeCode::Float64), + "STRING" => Some(TypeCode::String), + "BYTES" => Some(TypeCode::Bytes), + "DATE" => Some(TypeCode::Date), + "TIMESTAMP" => Some(TypeCode::Timestamp), + "UUID" => Some(TypeCode::Uuid), + "ENUM" => Some(TypeCode::Enum), + _ => None, + } +} + +/// Consumes lines belonging to a `part { ... }` block inside a Spanner key recipe and returns a [`Part`]. +/// +/// A part either represents a composite tag (`tag: `) or an individual column definition +/// with an ordering (`ASCENDING` / `DESCENDING`), a null ordering (`NULLS_FIRST`, `NULLS_LAST`, etc.), +/// and a Spanner type code (`BOOL`, `INT64`, `FLOAT64`, `STRING`, `BYTES`, etc.). +fn parse_part_block<'a, I: Iterator>(lines: &mut Peekable) -> Part { + let mut tag = 0u32; + let mut order = Order::Ascending; + let mut null_order = NullOrder::Unspecified; + let mut type_code = None; + let mut depth = 1; + + for line in lines.by_ref() { + let trimmed = line.trim(); + if trimmed.contains('{') { + depth += 1; + } + if trimmed == "}" || trimmed.ends_with('}') { + depth -= 1; + if depth == 0 { + break; + } + } + + if let Some(tag_string) = extract_value(trimmed, "tag:") { + if let Ok(tag_number) = tag_string.parse::() { + tag = tag_number; + } + } else if let Some(order_string) = extract_value(trimmed, "order:") { + order = match order_string { + "DESCENDING" => Order::Descending, + "ASCENDING" => Order::Ascending, + _ => order, + }; + } else if let Some(null_order_string) = extract_value(trimmed, "null_order:") { + null_order = match null_order_string { + "NULLS_FIRST" => NullOrder::NullsFirst, + "NULLS_LAST" => NullOrder::NullsLast, + "NOT_NULL" => NullOrder::NotNull, + _ => null_order, + }; + } else if let Some(code_string) = extract_value(trimmed, "code:") { + type_code = parse_type_code(code_string); + } + } + + let mut part = Part::new(); + if tag != 0 { + part = part.set_tag(tag); + } else { + part = part.set_order(order).set_null_order(null_order); + if let Some(code) = type_code { + part = part.set_type(Type::default().set_code(code)); + } + } + part +} + +/// Consumes lines belonging to a `test { ... }` block inside a test case and returns a [`ParsedTest`] +/// if it represents a simple key evaluation (`key { ... }`). +/// +/// This helper filters out more advanced routing test structures (such as `key_range`, `key_set`, +/// and `query_params`) so that we only execute direct conformance tests against `encode_key_from_recipe`. +fn parse_test_block<'a, I: Iterator>( + lines: &mut Peekable, +) -> Option { + let mut values = Vec::new(); + let mut start = None; + let mut approximate = false; + let mut is_simple_key = false; + let mut depth = 1; + + for line in lines.by_ref() { + let trimmed = line.trim(); + if trimmed.contains('{') { + depth += 1; + } + if trimmed == "}" || trimmed.ends_with('}') { + depth -= 1; + if depth == 0 { + break; + } + } + + if trimmed.starts_with("key {") { + is_simple_key = true; + } else if trimmed.starts_with("key_range {") + || trimmed.starts_with("key_set {") + || trimmed.starts_with("query_params {") + { + is_simple_key = false; + } else if let Some(start_string) = extract_value(trimmed, "start:") { + start = Some(unescape_bytes(start_string)); + } else if let Some(boolean_string) = extract_value(trimmed, "bool_value:") { + values.push((boolean_string == "true").to_value()); + } else if let Some(string_value) = extract_value(trimmed, "string_value:") { + values.push(string_value.to_value()); + } else if let Some(number_string) = extract_value(trimmed, "number_value:") { + if let Ok(num) = number_string.parse::() { + values.push(num.to_value()); + } + } else if trimmed == "null_value: NULL_VALUE" { + values.push(Value::null()); + } else if trimmed == "approximate: true" { + approximate = true; + } + } + + if let (true, Some(start_bytes)) = (is_simple_key, start) { + Some(ParsedTest { + values, + start: start_bytes, + approximate, + }) + } else { + None + } +} + +/// Consumes lines belonging to a top-level `test_case { ... }` block and coordinates parsing of its +/// name, recipe parts, and conformance test executions. +fn parse_test_case_block<'a, I: Iterator>( + lines: &mut Peekable, +) -> Option { + let mut name = String::new(); + let mut parts = Vec::new(); + let mut tests = Vec::new(); + let mut depth = 1; + + while let Some(line) = lines.next() { + let trimmed = line.trim(); + if !trimmed.starts_with("part {") && !trimmed.starts_with("test {") { + if trimmed.contains('{') { + depth += 1; + } + if trimmed == "}" || trimmed.ends_with('}') { + depth -= 1; + if depth == 0 { + break; + } + } + } + + if let Some(n) = extract_value(trimmed, "name:") { + name = n.to_string(); + } else if trimmed.starts_with("part {") { + parts.push(parse_part_block(lines)); + } else if trimmed.starts_with("test {") + && let Some(test) = parse_test_block(lines) + { + tests.push(test); + } + } + + if !name.is_empty() && !parts.is_empty() { + Some(ParsedTestCase { + name, + recipe: KeyRecipe::new().set_part(parts), + tests, + }) + } else { + None + } +} + +/// Parses a subset of `recipe_test.textproto` sufficient for golden conformance testing. +fn parse_golden_textproto(content: &str) -> Vec { + let mut cases = Vec::new(); + let mut lines = content.lines().peekable(); + + while let Some(line) = lines.next() { + if line.trim().starts_with("test_case {") + && let Some(case) = parse_test_case_block(&mut lines) + { + cases.push(case); + } + } + + cases +} + +#[test] +fn golden_conformance_supported_types() { + let path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("src") + .join("routing") + .join("testdata") + .join("recipe_test.textproto"); + + let textproto = fs::read_to_string(&path) + .expect("failed to load Spanner golden testdata from recipe_test.textproto"); + + let cases = parse_golden_textproto(&textproto); + assert!( + !cases.is_empty(), + "must parse at least one golden test case" + ); + + // Only execute golden conformance tests for data types currently supported by `encode_key_from_recipe`. + // Binary ssformat encoding for DATE, TIMESTAMP, UUID, and ENUM is not yet supported and will be + // added in subsequent pull requests. + let supported_test_prefixes = [ + "DataTypeTest_BOOL", + "DataTypeTest_INT64", + "DataTypeTest_FLOAT64", + "DataTypeTest_STRING", + "DataTypeTest_BYTES", + ]; + + let mut executed_tests = 0; + + for case in cases { + if !supported_test_prefixes + .iter() + .any(|prefix| case.name.starts_with(prefix)) + { + continue; + } + + for (index, test) in case.tests.iter().enumerate() { + // In Spanner's `recipe_test.textproto`, tests marked `approximate: true` represent + // cases where an invalid value type was provided (e.g., passing string `"true"` for a + // `BOOL` column). Spanner's router handles this by falling back to a partial prefix + // range lookup using only the preceding table/index tags. Because our `encode_key_from_recipe` + // method strictly verifies types and returns `Err` on type mismatch rather than truncating + // the key, we skip approximate prefix tests in full-key conformance verification. + if test.approximate { + continue; + } + + let encoded = match encode_key_from_recipe(&case.recipe, &test.values) { + Ok(bytes) => bytes, + Err(e) => panic!( + "Golden test case {} index {} failed encoding: {}", + case.name, index, e + ), + }; + + assert_eq!( + &encoded, &test.start, + "Mismatch in golden test case {} at test index {}: expected {:?}, got {:?}", + case.name, index, test.start, encoded + ); + executed_tests += 1; + } + } + + assert!( + executed_tests > 20, + "Expected to execute over 20 golden tests for supported types, executed {executed_tests}" + ); +} diff --git a/src/spanner/src/routing/mod.rs b/src/spanner/src/routing/mod.rs index 5917587fd4..7b86d7a7ce 100644 --- a/src/spanner/src/routing/mod.rs +++ b/src/spanner/src/routing/mod.rs @@ -17,5 +17,6 @@ pub(crate) mod connection_cache; pub(crate) mod endpoint_cooldown; pub(crate) mod key_range_cache; +pub(crate) mod key_recipe; pub(crate) mod server_connection; pub(crate) mod ssformat; diff --git a/src/spanner/src/routing/ssformat.rs b/src/spanner/src/routing/ssformat.rs index e0aefb9988..5f994e1d8e 100644 --- a/src/spanner/src/routing/ssformat.rs +++ b/src/spanner/src/routing/ssformat.rs @@ -56,8 +56,8 @@ const ASCENDING_FF_ESCAPE: u8 = 0x10; const SEP: u8 = 0x78; // 'x' // Composite tag validation constants -const K_OBJECT_EXISTENCE_TAG: i32 = 0x7e; -const K_MAX_FIELD_TAG: i32 = 0xffff; +const K_OBJECT_EXISTENCE_TAG: u32 = 0x7e; +const K_MAX_FIELD_TAG: u32 = 0xffff; // Offset to make negative timestamp seconds sort correctly const TIMESTAMP_SECONDS_OFFSET: i64 = i64::MIN; @@ -87,8 +87,8 @@ pub(crate) fn make_prefix_successor(key: &[u8]) -> Vec { } /// Appends a composite tag to the output buffer. -pub(crate) fn append_composite_tag(out: &mut Vec, tag: i32) -> Result<()> { - if tag == K_OBJECT_EXISTENCE_TAG || tag <= 0 || tag > K_MAX_FIELD_TAG { +pub(crate) fn append_composite_tag(out: &mut Vec, tag: u32) -> Result<()> { + if tag == K_OBJECT_EXISTENCE_TAG || tag == 0 || tag > K_MAX_FIELD_TAG { return Err(crate::error::internal_error(format!( "Invalid tag value: {tag}" ))); @@ -377,7 +377,7 @@ mod tests { #[test] fn append_composite_tag_invalid_tags() { - let invalid_tags = [0, -1, K_OBJECT_EXISTENCE_TAG, K_MAX_FIELD_TAG + 1]; + let invalid_tags = [0, K_OBJECT_EXISTENCE_TAG, K_MAX_FIELD_TAG + 1, u32::MAX]; for &tag in &invalid_tags { let mut out = Vec::new(); assert!( diff --git a/src/spanner/src/routing/testdata/recipe_test.textproto b/src/spanner/src/routing/testdata/recipe_test.textproto new file mode 100644 index 0000000000..43fae04f5e --- /dev/null +++ b/src/spanner/src/routing/testdata/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