google_cloud_spanner/row.rs
1// Copyright 2026 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::result_set_metadata::ResultSetMetadata;
16use crate::value::Value;
17
18/// A row in a query result.
19#[derive(Clone, Debug, PartialEq)]
20pub struct Row {
21 pub(crate) values: Vec<Value>,
22 pub(crate) metadata: ResultSetMetadata,
23}
24
25pub(crate) mod private {
26 /// A sealed trait to prevent external implementation of `ColumnIndex`.
27 pub trait Sealed {}
28 impl Sealed for usize {}
29 impl Sealed for &str {}
30 impl Sealed for String {}
31}
32
33/// A trait for types that can be used to index into a [`Row`].
34///
35/// This trait is sealed and cannot be implemented for types outside of this crate.
36pub trait ColumnIndex: private::Sealed + std::fmt::Debug {
37 /// Returns the index of the column in the given row, if it exists.
38 fn index(&self, row: &Row) -> Option<usize>;
39}
40
41impl ColumnIndex for usize {
42 fn index(&self, _row: &Row) -> Option<usize> {
43 Some(*self)
44 }
45}
46
47impl ColumnIndex for &str {
48 fn index(&self, row: &Row) -> Option<usize> {
49 row.metadata
50 .column_names
51 .iter()
52 .position(|name| name == *self)
53 }
54}
55
56impl ColumnIndex for String {
57 fn index(&self, row: &Row) -> Option<usize> {
58 self.as_str().index(row)
59 }
60}
61
62/// Errors that can occur when getting a value from a [`Row`].
63#[derive(thiserror::Error, Debug)]
64#[non_exhaustive]
65pub enum RowError {
66 /// The requested column name or index was not found in the row.
67 #[error("Could not find column with index: {0}")]
68 ColumnNotFound(String),
69 /// The requested column index was out of range.
70 #[error("Column index out of range: {index} (expected < {len})")]
71 IndexOutOfRange { index: usize, len: usize },
72}
73
74impl Row {
75 /// Returns the raw values of the row.
76 pub fn raw_values(&self) -> &[Value] {
77 &self.values
78 }
79
80 /// Returns true if the value at the specified column name or index is null.
81 ///
82 /// # Example
83 /// ```
84 /// # use google_cloud_spanner::client::Spanner;
85 /// # use google_cloud_spanner::statement::Statement;
86 /// # async fn test_doc() -> anyhow::Result<()> {
87 /// let client = Spanner::builder().build().await?;
88 /// let db_client = client.database_client("projects/p/instances/i/databases/d").build().await?;
89 /// let transaction = db_client.single_use().build();
90 /// let mut result_set = transaction.execute_query(Statement::builder("SELECT NULL AS Age").build()).await?;
91 ///
92 /// if let Some(row) = result_set.next().await {
93 /// let is_null = row?.try_is_null("Age")?;
94 /// println!("Is null: {}", is_null);
95 /// }
96 /// # Ok(())
97 /// # }
98 /// ```
99 ///
100 /// # Arguments
101 ///
102 /// * `index` - The column name (string) or index (zero-based integer).
103 ///
104 /// # Returns
105 ///
106 /// * `Ok(bool)` if the value is null or not.
107 /// * `Err(Error)` if the column name or index is invalid.
108 pub fn try_is_null<I: ColumnIndex>(&self, index: I) -> crate::Result<bool> {
109 let (_, value) = self.get_value(index)?;
110 Ok(value.kind() == crate::value::Kind::Null)
111 }
112
113 /// Returns true if the value at the specified column name or index is null, panicking on error.
114 ///
115 /// # Example
116 /// ```
117 /// # use google_cloud_spanner::client::Spanner;
118 /// # use google_cloud_spanner::statement::Statement;
119 /// # async fn test_doc() -> anyhow::Result<()> {
120 /// let client = Spanner::builder().build().await?;
121 /// let db_client = client.database_client("projects/p/instances/i/databases/d").build().await?;
122 /// let transaction = db_client.single_use().build();
123 /// let mut result_set = transaction.execute_query(Statement::builder("SELECT NULL AS Age").build()).await?;
124 ///
125 /// if let Some(row) = result_set.next().await {
126 /// let is_null = row?.is_null("Age");
127 /// println!("Is null: {}", is_null);
128 /// }
129 /// # Ok(())
130 /// # }
131 /// ```
132 ///
133 /// This is a convenience wrapper around [`try_is_null`](Row::try_is_null).
134 ///
135 /// # Panics
136 ///
137 /// Panics if the column name or index is invalid.
138 pub fn is_null<I: ColumnIndex>(&self, index: I) -> bool {
139 self.try_is_null(index).unwrap()
140 }
141
142 /// Retrieves a value from the row by column name or zero-based index.
143 ///
144 /// # Example
145 /// ```
146 /// # use google_cloud_spanner::client::Spanner;
147 /// # use google_cloud_spanner::statement::Statement;
148 /// # async fn test_doc() -> anyhow::Result<()> {
149 /// let client = Spanner::builder().build().await?;
150 /// let db_client = client.database_client("projects/p/instances/i/databases/d").build().await?;
151 /// let transaction = db_client.single_use().build();
152 /// let mut result_set = transaction.execute_query(Statement::builder("SELECT 42 AS Age").build()).await?;
153 ///
154 /// if let Some(row) = result_set.next().await {
155 /// let age: i64 = row?.try_get("Age")?;
156 /// println!("Age: {}", age);
157 /// }
158 /// # Ok(())
159 /// # }
160 /// ```
161 ///
162 /// # Arguments
163 ///
164 /// * `index` - The column name (string) or index (zero-based integer).
165 ///
166 /// # Returns
167 ///
168 /// * `Ok(T)` if the value was successfully retrieved and converted to type `T`.
169 /// * `Err(Error)` if:
170 /// * The column name or index is invalid.
171 /// * The column value is incompatible with type `T`.
172 pub fn try_get<T: crate::from_value::FromValue, I: ColumnIndex>(
173 &self,
174 index: I,
175 ) -> crate::Result<T> {
176 let (idx, value) = self.get_value(index)?;
177 let r#type = self.metadata.column_types.get(idx).ok_or_else(|| {
178 crate::Error::deser(RowError::IndexOutOfRange {
179 index: idx,
180 len: self.metadata.column_types.len(),
181 })
182 })?;
183 T::from_value(value, r#type).map_err(crate::Error::deser)
184 }
185
186 /// Retrieves a value from the row by column name or zero-based index, panicking on error.
187 ///
188 /// # Example
189 /// ```
190 /// # use google_cloud_spanner::client::Spanner;
191 /// # use google_cloud_spanner::statement::Statement;
192 /// # async fn test_doc() -> anyhow::Result<()> {
193 /// let client = Spanner::builder().build().await?;
194 /// let db_client = client.database_client("projects/p/instances/i/databases/d").build().await?;
195 /// let transaction = db_client.single_use().build();
196 /// let mut result_set = transaction.execute_query(Statement::builder("SELECT 42 AS Age").build()).await?;
197 ///
198 /// if let Some(row) = result_set.next().await {
199 /// let age: i64 = row?.get("Age");
200 /// println!("Age: {}", age);
201 /// }
202 /// # Ok(())
203 /// # }
204 /// ```
205 ///
206 /// This is a convenience wrapper around [`try_get`](Row::try_get).
207 ///
208 /// # Panics
209 ///
210 /// Panics if:
211 /// * The column name or index is invalid.
212 /// * The column value is incompatible with type `T`.
213 pub fn get<T: crate::from_value::FromValue, I: ColumnIndex>(&self, index: I) -> T {
214 self.try_get(index).unwrap()
215 }
216
217 fn get_value<I: ColumnIndex>(&self, index: I) -> crate::Result<(usize, &Value)> {
218 let idx = index
219 .index(self)
220 .ok_or_else(|| crate::Error::deser(RowError::ColumnNotFound(format!("{:?}", index))))?;
221 let value = self.values.get(idx).ok_or_else(|| {
222 crate::Error::deser(RowError::IndexOutOfRange {
223 index: idx,
224 len: self.values.len(),
225 })
226 })?;
227 Ok((idx, value))
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use crate::to_value::ToValue;
235 use crate::types;
236 use rust_decimal::Decimal;
237 use std::sync::Arc;
238 use time::{Date, Month, OffsetDateTime};
239
240 #[test]
241 fn auto_traits() {
242 static_assertions::assert_impl_all!(Row: Clone, std::fmt::Debug, PartialEq, Send, Sync);
243 }
244
245 #[test]
246 fn row_get() {
247 let names = vec![
248 "col_string".to_string(),
249 "col_int64".to_string(),
250 "col_float64".to_string(),
251 "col_bool".to_string(),
252 "col_bytes".to_string(),
253 "col_numeric".to_string(),
254 "col_date".to_string(),
255 "col_timestamp".to_string(),
256 "col_float32".to_string(),
257 "col_json".to_string(),
258 "col_uuid".to_string(),
259 "col_interval".to_string(),
260 ];
261
262 let types = vec![
263 types::string(),
264 types::int64(),
265 types::float64(),
266 types::bool(),
267 types::bytes(),
268 types::numeric(),
269 types::date(),
270 types::timestamp(),
271 types::float32(),
272 types::json(),
273 types::uuid(),
274 types::interval(),
275 ];
276
277 let d = Decimal::from_str_exact("123.456").unwrap();
278 let dt = Date::from_calendar_date(2023, Month::October, 27).unwrap();
279 let ts = OffsetDateTime::parse(
280 "2023-10-27T10:00:00Z",
281 &time::format_description::well_known::Rfc3339,
282 )
283 .unwrap();
284
285 let values = vec![
286 "hello".to_string().to_value(),
287 42_i64.to_value(),
288 42.5_f64.to_value(),
289 true.to_value(),
290 vec![1_u8, 2, 3].to_value(),
291 d.to_value(),
292 dt.to_value(),
293 ts.to_value(),
294 1.23_f32.to_value(),
295 "{\"key\":\"value\"}".to_string().to_value(),
296 "123e4567-e89b-12d3-a456-426614174000"
297 .to_string()
298 .to_value(),
299 "P1Y2M3D".to_string().to_value(),
300 ];
301
302 let row = Row {
303 values,
304 metadata: ResultSetMetadata {
305 column_names: Arc::new(names),
306 column_types: Arc::new(types),
307 undeclared_parameters: Arc::new(std::collections::BTreeMap::new()),
308 },
309 };
310
311 // Test getting by valid index
312 assert_eq!(row.get::<String, _>(0), "hello");
313 assert_eq!(row.get::<i64, _>(1), 42);
314 assert_eq!(row.get::<f64, _>(2), 42.5);
315 assert!(row.get::<bool, _>(3));
316 assert_eq!(row.get::<Vec<u8>, _>(4), vec![1_u8, 2, 3]);
317 assert_eq!(row.get::<Decimal, _>(5), d);
318 assert_eq!(row.get::<Date, _>(6), dt);
319 assert_eq!(row.get::<OffsetDateTime, _>(7), ts);
320 assert_eq!(row.get::<f32, _>(8), 1.23_f32);
321 assert_eq!(row.get::<String, _>(9), "{\"key\":\"value\"}");
322 assert_eq!(
323 row.get::<String, _>(10),
324 "123e4567-e89b-12d3-a456-426614174000"
325 );
326 assert_eq!(row.get::<String, _>(11), "P1Y2M3D");
327
328 // Test getting by valid name
329 assert_eq!(row.get::<String, _>("col_string"), "hello");
330 assert_eq!(row.get::<i64, _>("col_int64"), 42);
331 assert_eq!(row.get::<f64, _>("col_float64"), 42.5);
332 assert!(row.get::<bool, _>("col_bool"));
333 assert_eq!(row.get::<Vec<u8>, _>("col_bytes"), vec![1_u8, 2, 3]);
334 assert_eq!(row.get::<Decimal, _>("col_numeric"), d);
335 assert_eq!(row.get::<Date, _>("col_date"), dt);
336 assert_eq!(row.get::<OffsetDateTime, _>("col_timestamp"), ts);
337 assert_eq!(row.get::<f32, _>("col_float32"), 1.23_f32);
338 assert_eq!(row.get::<String, _>("col_json"), "{\"key\":\"value\"}");
339 assert_eq!(
340 row.get::<String, _>("col_uuid"),
341 "123e4567-e89b-12d3-a456-426614174000"
342 );
343 assert_eq!(row.get::<String, _>("col_interval"), "P1Y2M3D");
344
345 // Test getting by invalid index
346 assert!(row.try_get::<String, _>(12).is_err());
347
348 // Test getting by invalid name
349 assert!(row.try_get::<String, _>("col_invalid").is_err());
350
351 // Test getting mismatched type
352 assert!(row.try_get::<i64, _>(0).is_err());
353 assert!(row.try_get::<bool, _>(1).is_err());
354
355 // int64 is encoded as a string, so getting it as a string is also possible.
356 assert_eq!(row.get::<String, _>(1), "42");
357 }
358}