1use crate::types::SpannerType;
10use gcloud_googleapis::spanner::v1::{Type, TypeCode};
11use gcloud_spanner::statement::{ToKind, single_type};
12use prost_types::value::Kind;
13use serde_json::Value;
14
15pub struct EncodedKind(pub Kind);
19
20impl ToKind for EncodedKind {
21 fn to_kind(&self) -> Kind {
22 self.0.clone()
23 }
24 fn get_type() -> Type
25 where
26 Self: Sized,
27 {
28 single_type(TypeCode::String)
29 }
30}
31
32pub fn encode_to_kind(value: &Value, ty: &SpannerType) -> Result<Kind, String> {
37 if value.is_null() {
38 return Ok(Kind::NullValue(0));
39 }
40 match ty {
41 SpannerType::Bool => match value {
42 Value::Bool(b) => Ok(Kind::BoolValue(*b)),
43 other => Err(mismatch("BOOL", other)),
44 },
45 SpannerType::Int64 => match value {
47 Value::Number(n) => {
48 if let Some(i) = n.as_i64() {
49 Ok(Kind::StringValue(i.to_string()))
50 } else if let Some(u) = n.as_u64() {
51 i64::try_from(u)
53 .map(|i| Kind::StringValue(i.to_string()))
54 .map_err(|_| format!("integer {u} overflows INT64"))
55 } else {
56 Err(mismatch("INT64", value))
57 }
58 }
59 Value::String(s) if s.parse::<i64>().is_ok() => Ok(Kind::StringValue(s.clone())),
61 other => Err(mismatch("INT64", other)),
62 },
63 SpannerType::Float32 | SpannerType::Float64 => match value {
64 Value::Number(n) => n
65 .as_f64()
66 .map(Kind::NumberValue)
67 .ok_or_else(|| mismatch("FLOAT64", value)),
68 Value::String(s) if matches!(s.as_str(), "NaN" | "Infinity" | "-Infinity") => {
70 Ok(Kind::StringValue(s.clone()))
71 }
72 other => Err(mismatch("FLOAT64", other)),
73 },
74 SpannerType::String => match value {
75 Value::String(s) => Ok(Kind::StringValue(s.clone())),
76 Value::Number(n) => Ok(Kind::StringValue(n.to_string())),
79 Value::Bool(b) => Ok(Kind::StringValue(b.to_string())),
80 other => Ok(Kind::StringValue(other.to_string())),
81 },
82 SpannerType::Timestamp | SpannerType::Date | SpannerType::Bytes => match value {
85 Value::String(s) => Ok(Kind::StringValue(s.clone())),
86 other => Err(mismatch(type_name(ty), other)),
87 },
88 SpannerType::Numeric => match value {
89 Value::String(s) => Ok(Kind::StringValue(s.clone())),
90 Value::Number(n) => Ok(Kind::StringValue(n.to_string())),
91 other => Err(mismatch("NUMERIC", other)),
92 },
93 SpannerType::Json => Ok(Kind::StringValue(value.to_string())),
95 SpannerType::Array(inner) => match value {
96 Value::Array(items) => {
97 let values: Result<Vec<prost_types::Value>, String> = items
98 .iter()
99 .map(|item| {
100 encode_to_kind(item, inner)
101 .map(|kind| prost_types::Value { kind: Some(kind) })
102 })
103 .collect();
104 Ok(Kind::ListValue(prost_types::ListValue { values: values? }))
105 }
106 other => Err(mismatch("ARRAY", other)),
107 },
108 SpannerType::Other => Err(
109 "unsupported destination column type (STRUCT/PROTO columns are not writable)".into(),
110 ),
111 }
112}
113
114fn type_name(ty: &SpannerType) -> &'static str {
115 match ty {
116 SpannerType::Timestamp => "TIMESTAMP",
117 SpannerType::Date => "DATE",
118 SpannerType::Bytes => "BYTES",
119 _ => "value",
120 }
121}
122
123fn mismatch(expected: &str, got: &Value) -> String {
124 let got_ty = match got {
125 Value::Null => "null",
126 Value::Bool(_) => "boolean",
127 Value::Number(_) => "number",
128 Value::String(_) => "string",
129 Value::Array(_) => "array",
130 Value::Object(_) => "object",
131 };
132 format!("expected {expected}-compatible value, got {got_ty}")
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use serde_json::json;
139
140 #[test]
141 fn null_encodes_as_null_for_every_type() {
142 for ty in [
143 SpannerType::Bool,
144 SpannerType::Int64,
145 SpannerType::Json,
146 SpannerType::Array(Box::new(SpannerType::String)),
147 ] {
148 assert!(matches!(
149 encode_to_kind(&Value::Null, &ty).unwrap(),
150 Kind::NullValue(_)
151 ));
152 }
153 }
154
155 #[test]
156 fn int64_encodes_as_string_losslessly() {
157 let big = 9_007_199_254_740_993_i64;
158 assert_eq!(
159 encode_to_kind(&json!(big), &SpannerType::Int64).unwrap(),
160 Kind::StringValue(big.to_string())
161 );
162 assert_eq!(
163 encode_to_kind(&json!("42"), &SpannerType::Int64).unwrap(),
164 Kind::StringValue("42".into())
165 );
166 assert!(encode_to_kind(&json!(1.5), &SpannerType::Int64).is_err());
167 assert!(encode_to_kind(&json!("abc"), &SpannerType::Int64).is_err());
168 assert!(
169 encode_to_kind(&json!(u64::MAX), &SpannerType::Int64)
170 .unwrap_err()
171 .contains("overflows INT64")
172 );
173 }
174
175 #[test]
176 fn floats_bools_and_special_values() {
177 assert_eq!(
178 encode_to_kind(&json!(2.5), &SpannerType::Float64).unwrap(),
179 Kind::NumberValue(2.5)
180 );
181 assert_eq!(
182 encode_to_kind(&json!("NaN"), &SpannerType::Float64).unwrap(),
183 Kind::StringValue("NaN".into())
184 );
185 assert!(encode_to_kind(&json!("2.5"), &SpannerType::Float64).is_err());
186 assert_eq!(
187 encode_to_kind(&json!(true), &SpannerType::Bool).unwrap(),
188 Kind::BoolValue(true)
189 );
190 assert!(encode_to_kind(&json!(1), &SpannerType::Bool).is_err());
191 }
192
193 #[test]
194 fn strings_coerce_scalars_and_serialize_containers() {
195 assert_eq!(
196 encode_to_kind(&json!("x"), &SpannerType::String).unwrap(),
197 Kind::StringValue("x".into())
198 );
199 assert_eq!(
200 encode_to_kind(&json!(7), &SpannerType::String).unwrap(),
201 Kind::StringValue("7".into())
202 );
203 assert_eq!(
204 encode_to_kind(&json!({"a": 1}), &SpannerType::String).unwrap(),
205 Kind::StringValue("{\"a\":1}".into())
206 );
207 }
208
209 #[test]
210 fn timestamp_date_bytes_require_strings() {
211 assert_eq!(
212 encode_to_kind(&json!("2026-01-01T00:00:00Z"), &SpannerType::Timestamp).unwrap(),
213 Kind::StringValue("2026-01-01T00:00:00Z".into())
214 );
215 assert!(encode_to_kind(&json!(5), &SpannerType::Timestamp).is_err());
216 assert!(encode_to_kind(&json!(5), &SpannerType::Date).is_err());
217 assert!(encode_to_kind(&json!(5), &SpannerType::Bytes).is_err());
218 }
219
220 #[test]
221 fn numeric_accepts_strings_and_numbers() {
222 assert_eq!(
223 encode_to_kind(&json!("123.456789012345"), &SpannerType::Numeric).unwrap(),
224 Kind::StringValue("123.456789012345".into())
225 );
226 assert_eq!(
227 encode_to_kind(&json!(12), &SpannerType::Numeric).unwrap(),
228 Kind::StringValue("12".into())
229 );
230 assert!(encode_to_kind(&json!(true), &SpannerType::Numeric).is_err());
231 }
232
233 #[test]
234 fn json_columns_take_any_value() {
235 assert_eq!(
236 encode_to_kind(&json!({"a": [1]}), &SpannerType::Json).unwrap(),
237 Kind::StringValue("{\"a\":[1]}".into())
238 );
239 assert_eq!(
241 encode_to_kind(&json!("s"), &SpannerType::Json).unwrap(),
242 Kind::StringValue("\"s\"".into())
243 );
244 }
245
246 #[test]
247 fn arrays_recurse_and_reject_non_arrays() {
248 let ty = SpannerType::Array(Box::new(SpannerType::Int64));
249 let Kind::ListValue(list) = encode_to_kind(&json!([1, null, 3]), &ty).unwrap() else {
250 panic!("expected list");
251 };
252 assert_eq!(list.values.len(), 3);
253 assert!(encode_to_kind(&json!(1), &ty).is_err());
254 assert!(encode_to_kind(&json!(["x"]), &ty).is_err());
256 }
257
258 #[test]
259 fn unsupported_destination_types_error() {
260 assert!(encode_to_kind(&json!(1), &SpannerType::Other).is_err());
261 }
262
263 #[test]
264 fn encoded_kind_is_a_usable_tokind() {
265 let ek = EncodedKind(Kind::StringValue("v".into()));
266 assert_eq!(ek.to_kind(), Kind::StringValue("v".into()));
267 let dyn_ref: &dyn ToKind = &ek;
268 assert_eq!(dyn_ref.to_kind(), Kind::StringValue("v".into()));
269 }
270}