From ae13bb65d9a221612ca6c80f8637c348feabcd29 Mon Sep 17 00:00:00 2001 From: hyx Date: Sat, 24 Jan 2026 14:59:12 +0900 Subject: [PATCH 1/3] feat(spanner): add uuid type support Add optional uuid feature to support reading/writing UUID types from Spanner. The emulator supports UUID since v1.5.46, but the Rust SDK lacked TryFromValue implementation. - Add uuid crate as optional dependency with 'uuid' feature flag - Implement TryFromValue for uuid::Uuid (parse from StringValue) - Implement ToKind for uuid::Uuid (serialize to StringValue with TypeCode::Uuid) - Add tests for both serialization and deserialization --- spanner/Cargo.toml | 2 ++ spanner/src/row.rs | 39 +++++++++++++++++++++++++++++++++++++++ spanner/src/statement.rs | 24 ++++++++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/spanner/Cargo.toml b/spanner/Cargo.toml index 8d626a2f..539a5d6b 100644 --- a/spanner/Cargo.toml +++ b/spanner/Cargo.toml @@ -23,6 +23,7 @@ parking_lot = "0.12" base64 = "0.22" tokio-util = "0.7" bigdecimal = { version="0.4", features=["serde"] } +uuid = { version = "1", optional = true } token-source = "1.0" google-cloud-longrunning = { package = "gcloud-longrunning", version = "1.3.1", path = "../foundation/longrunning" } @@ -49,3 +50,4 @@ external-account = ["google-cloud-auth?/external-account"] otel-metrics = ["opentelemetry"] jwt-aws-lc-rs = ["google-cloud-auth?/jwt-aws-lc-rs"] jwt-rust-crypto = ["google-cloud-auth?/jwt-rust-crypto"] +uuid = ["dep:uuid"] diff --git a/spanner/src/row.rs b/spanner/src/row.rs index da5caf40..7afd0d29 100644 --- a/spanner/src/row.rs +++ b/spanner/src/row.rs @@ -52,6 +52,9 @@ pub enum Error { BigDecimalParseError(String, #[source] ParseBigDecimalError), #[error("Failed to parse as Prost Timestamp field={0}")] ProstTimestampParseError(String, #[source] ::prost_types::TimestampError), + #[cfg(feature = "uuid")] + #[error("Failed to parse as Uuid field={0}")] + UuidParseError(String, #[source] ::uuid::Error), } impl Row { @@ -231,6 +234,18 @@ impl TryFromValue for BigDecimal { } } +#[cfg(feature = "uuid")] +impl TryFromValue for ::uuid::Uuid { + fn try_from(item: &Value, field: &Field) -> Result { + match as_ref(item, field)? { + Kind::StringValue(s) => { + ::uuid::Uuid::parse_str(s).map_err(|e| Error::UuidParseError(field.name.to_string(), e)) + } + v => kind_to_error(v, field), + } + } +} + impl TryFromValue for String { fn try_from(item: &Value, field: &Field) -> Result { match as_ref(item, field)? { @@ -491,4 +506,28 @@ mod tests { ); assert_eq!(format!("{}", struct_data[1].prost_timestamp), "2027-02-19T07:23:59Z"); } + + #[cfg(feature = "uuid")] + #[test] + fn test_uuid_try_from() { + use crate::row::TryFromValue; + use crate::statement::ToKind; + use google_cloud_googleapis::spanner::v1::struct_type::Field; + use prost_types::Value; + + let uuid_str = "550e8400-e29b-41d4-a716-446655440000"; + let expected_uuid = ::uuid::Uuid::parse_str(uuid_str).unwrap(); + + let field = Field { + name: "uuid_field".to_string(), + r#type: Some(::uuid::Uuid::get_type()), + }; + + let value = Value { + kind: Some(uuid_str.to_string().to_kind()), + }; + + let parsed: ::uuid::Uuid = TryFromValue::try_from(&value, &field).unwrap(); + assert_eq!(parsed, expected_uuid); + } } diff --git a/spanner/src/statement.rs b/spanner/src/statement.rs index a07ee1bb..be36a50d 100644 --- a/spanner/src/statement.rs +++ b/spanner/src/statement.rs @@ -207,6 +207,16 @@ impl ToKind for BigDecimal { } } +#[cfg(feature = "uuid")] +impl ToKind for ::uuid::Uuid { + fn to_kind(&self) -> Kind { + self.to_string().to_kind() + } + fn get_type() -> Type { + single_type(TypeCode::Uuid) + } +} + impl ToKind for ::prost_types::Timestamp { fn to_kind(&self) -> Kind { // The protobuf timestamp type should be formatted in RFC3339 @@ -328,4 +338,18 @@ mod test { // Prost's Timestamp type and OffsetDateTime should have the same representation in spanner assert_eq!(prost_types::Timestamp::get_type(), OffsetDateTime::get_type()); } + + #[cfg(feature = "uuid")] + #[test] + fn uuid_to_kind_works() { + use google_cloud_googleapis::spanner::v1::TypeCode; + + let uuid = ::uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap(); + let expected = "550e8400-e29b-41d4-a716-446655440000"; + let kind = uuid.to_kind(); + assert!(matches!(kind, Kind::StringValue(s) if s == expected)); + + let uuid_type = ::uuid::Uuid::get_type(); + assert_eq!(uuid_type.code, TypeCode::Uuid as i32); + } } From 3b8c1498159b192df4e72c79cd3df4b0011b8065 Mon Sep 17 00:00:00 2001 From: hyx Date: Sat, 24 Jan 2026 15:26:03 +0900 Subject: [PATCH 2/3] fix(spanner): validate TypeCode before UUID parsing Prevent UUID parsing from being attempted on non-UUID columns. Now TryFromValue for uuid::Uuid checks TypeCode::Uuid before parsing, returning a clear error if the column type doesn't match. --- spanner/src/row.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spanner/src/row.rs b/spanner/src/row.rs index 7afd0d29..eb1f9c61 100644 --- a/spanner/src/row.rs +++ b/spanner/src/row.rs @@ -237,6 +237,21 @@ impl TryFromValue for BigDecimal { #[cfg(feature = "uuid")] impl TryFromValue for ::uuid::Uuid { fn try_from(item: &Value, field: &Field) -> Result { + use google_cloud_googleapis::spanner::v1::TypeCode; + + let type_code = field + .r#type + .as_ref() + .map(|t| t.code) + .unwrap_or(TypeCode::Unspecified as i32); + + if type_code != TypeCode::Uuid as i32 { + return Err(Error::KindMismatch( + field.name.to_string(), + format!("expected UUID type, got TypeCode={}", type_code), + )); + } + match as_ref(item, field)? { Kind::StringValue(s) => { ::uuid::Uuid::parse_str(s).map_err(|e| Error::UuidParseError(field.name.to_string(), e)) From 3495c131a73b1d641f131908e7d15c72d47929d9 Mon Sep 17 00:00:00 2001 From: hyx Date: Sat, 24 Jan 2026 15:47:31 +0900 Subject: [PATCH 3/3] feat(spanner): add Row::field() and Row::field_by_name() getters Expose Field metadata to allow callers to inspect TypeCode before reading values. Useful for: - Distinguishing BYTES vs STRING (both use StringValue) - Checking array element types - Validating JSON column types --- spanner/src/row.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spanner/src/row.rs b/spanner/src/row.rs index eb1f9c61..74b6530a 100644 --- a/spanner/src/row.rs +++ b/spanner/src/row.rs @@ -75,6 +75,14 @@ impl Row { { self.column(index(&self.index, column_name)?) } + + pub fn field(&self, column_index: usize) -> Option<&Field> { + self.fields.get(column_index) + } + + pub fn field_by_name(&self, column_name: &str) -> Option<&Field> { + self.index.get(column_name).and_then(|&idx| self.fields.get(idx)) + } } //don't use TryFrom trait to avoid the conflict