Skip to main content

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}