diff --git a/spanner/Cargo.toml b/spanner/Cargo.toml index c3818c19..fe20c617 100644 --- a/spanner/Cargo.toml +++ b/spanner/Cargo.toml @@ -24,6 +24,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" } @@ -50,3 +51,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..74b6530a 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 { @@ -72,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 @@ -231,6 +242,33 @@ 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)) + } + 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 +529,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); + } }