google_cloud_spanner/read.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::key::KeySet;
16use crate::model::DirectedReadOptions;
17use crate::model::read_request::{LockHint, OrderBy};
18use crate::model::request_options::Priority;
19use google_cloud_gax::backoff_policy::BackoffPolicyArg;
20use google_cloud_gax::options::RequestOptions as GaxRequestOptions;
21use google_cloud_gax::retry_policy::RetryPolicyArg;
22use std::time::Duration;
23
24/// Represents an incomplete read operation that requires specifying keys.
25///
26/// # Example
27/// ```
28/// # use google_cloud_spanner::read::ReadRequest;
29/// # use google_cloud_spanner::key::KeySet;
30/// # use google_cloud_spanner::key;
31/// // Read all rows from a table using its primary key.
32/// let read_all = ReadRequest::builder("Users", vec!["Id", "Name"])
33/// .with_keys(KeySet::all())
34/// .build();
35///
36/// // Read specific rows using an index.
37/// let read_by_id = ReadRequest::builder("Users", vec!["Id", "Name"])
38/// .with_index("UsersByIndex", key![1_i64])
39/// .set_limit(10)
40/// .build();
41/// ```
42///
43/// Use `ReadRequest::builder` to define the table and columns to be read.
44/// Keys must be supplied using `with_keys` (for the primary key) or `with_index` (for an index)
45/// to obtain an executable `ReadRequest`.
46#[derive(Clone, Debug, PartialEq)]
47pub struct ReadRequestBuilder {
48 table: String,
49 columns: Vec<String>,
50}
51
52impl ReadRequestBuilder {
53 /// Supplies the `KeySet` targeting the table's primary key.
54 ///
55 /// # Example
56 /// ```
57 /// # use google_cloud_spanner::read::ReadRequest;
58 /// # use google_cloud_spanner::key::KeySet;
59 /// let request = ReadRequest::builder("Users", vec!["Id", "Name"]).with_keys(KeySet::all());
60 /// ```
61 ///
62 /// The `keys` parameter identifies the rows to be yielded by naming the primary keys
63 /// of the rows in the table. Rows are yielded in table primary key order.
64 pub fn with_keys(self, keys: impl Into<KeySet>) -> ConfiguredReadRequestBuilder {
65 ConfiguredReadRequestBuilder {
66 table: self.table,
67 index: None,
68 keys: keys.into(),
69 columns: self.columns,
70 limit: None,
71 request_options: None,
72 directed_read_options: None,
73 order_by: None,
74 lock_hint: None,
75 gax_options: GaxRequestOptions::default(),
76 }
77 }
78
79 /// Supplies an index name and its associated `KeySet`.
80 ///
81 /// # Example
82 /// ```
83 /// # use google_cloud_spanner::read::ReadRequest;
84 /// # use google_cloud_spanner::key::KeySet;
85 /// # use google_cloud_spanner::key;
86 /// let request = ReadRequest::builder("Users", vec!["Id", "Name"]).with_index("UsersByIndex", key![1_i64]);
87 /// ```
88 ///
89 /// The `keys` parameter identifies the rows to be yielded by naming the index keys
90 /// in the provided `index`. Rows are yielded in index key order.
91 pub fn with_index(
92 self,
93 index: impl Into<String>,
94 keys: impl Into<KeySet>,
95 ) -> ConfiguredReadRequestBuilder {
96 ConfiguredReadRequestBuilder {
97 table: self.table,
98 index: Some(index.into()),
99 keys: keys.into(),
100 columns: self.columns,
101 limit: None,
102 request_options: None,
103 directed_read_options: None,
104 order_by: None,
105 lock_hint: None,
106 gax_options: GaxRequestOptions::default(),
107 }
108 }
109}
110
111/// A fully configured read request that is ready to be built.
112#[derive(Clone, Debug)]
113pub struct ConfiguredReadRequestBuilder {
114 table: String,
115 index: Option<String>,
116 keys: KeySet,
117 columns: Vec<String>,
118 limit: Option<i64>,
119 request_options: Option<crate::model::RequestOptions>,
120 directed_read_options: Option<DirectedReadOptions>,
121 order_by: Option<OrderBy>,
122 lock_hint: Option<LockHint>,
123 gax_options: GaxRequestOptions,
124}
125
126impl ConfiguredReadRequestBuilder {
127 /// Sets an optional limit on how many rows could be retrieved.
128 ///
129 /// # Example
130 /// ```
131 /// # use google_cloud_spanner::read::ReadRequest;
132 /// # use google_cloud_spanner::key::KeySet;
133 /// let request = ReadRequest::builder("Users", vec!["Id"])
134 /// .with_keys(KeySet::all())
135 /// .set_limit(10)
136 /// .build();
137 /// ```
138 ///
139 /// If fewer rows are found, only the matching rows will be returned.
140 pub fn set_limit(mut self, limit: i64) -> Self {
141 self.limit = Some(limit);
142 self
143 }
144
145 /// Sets the request tag to use for this read.
146 ///
147 /// # Example
148 /// ```
149 /// # use google_cloud_spanner::read::ReadRequest;
150 /// # use google_cloud_spanner::key::KeySet;
151 /// let request = ReadRequest::builder("Users", vec!["Id"])
152 /// .with_keys(KeySet::all())
153 /// .set_request_tag("my-tag")
154 /// .build();
155 /// ```
156 ///
157 /// See also: [Troubleshooting with tags](https://docs.cloud.google.com/spanner/docs/introspection/troubleshooting-with-tags)
158 pub fn set_request_tag(mut self, tag: impl Into<String>) -> Self {
159 self.request_options
160 .get_or_insert_with(crate::model::RequestOptions::default)
161 .request_tag = tag.into();
162 self
163 }
164
165 /// Sets the RPC priority to use for this read request.
166 ///
167 /// # Example
168 /// ```
169 /// # use google_cloud_spanner::read::ReadRequest;
170 /// # use google_cloud_spanner::key::KeySet;
171 /// # use google_cloud_spanner::model::request_options::Priority;
172 /// let request = ReadRequest::builder("Users", vec!["Id"])
173 /// .with_keys(KeySet::all())
174 /// .set_priority(Priority::Low)
175 /// .build();
176 /// ```
177 pub fn set_priority(mut self, priority: Priority) -> Self {
178 self.request_options
179 .get_or_insert_with(crate::model::RequestOptions::default)
180 .priority = priority;
181 self
182 }
183
184 /// Sets the directed read options for this request.
185 ///
186 /// ```
187 /// # use google_cloud_spanner::read::ReadRequest;
188 /// # use google_cloud_spanner::key::KeySet;
189 /// # use google_cloud_spanner::model::DirectedReadOptions;
190 /// let dro = DirectedReadOptions::default();
191 /// let req = ReadRequest::builder("MyTable", vec!["col1"])
192 /// .with_keys(KeySet::all())
193 /// .set_directed_read_options(dro)
194 /// .build();
195 /// ```
196 ///
197 /// DirectedReadOptions can only be specified for a read-only transaction,
198 /// otherwise Spanner returns an INVALID_ARGUMENT error.
199 pub fn set_directed_read_options(mut self, options: DirectedReadOptions) -> Self {
200 self.directed_read_options = Some(options);
201 self
202 }
203
204 /// Sets the order in which rows are returned.
205 ///
206 /// # Example
207 /// ```
208 /// # use google_cloud_spanner::read::ReadRequest;
209 /// # use google_cloud_spanner::key::KeySet;
210 /// # use google_cloud_spanner::model::read_request::OrderBy;
211 /// let request = ReadRequest::builder("Users", vec!["Id"])
212 /// .with_keys(KeySet::all())
213 /// .set_order_by(OrderBy::NoOrder);
214 /// ```
215 ///
216 /// By default, Spanner returns result rows in primary key order (or index key
217 /// order if reading via an index) except for `PartitionRead` requests.
218 ///
219 /// For applications that don't require rows to be returned in primary key
220 /// (`ORDER_BY_PRIMARY_KEY`) order, setting `ORDER_BY_NO_ORDER` option allows
221 /// Spanner to optimize row retrieval, resulting in lower latencies in certain
222 /// cases (for example, bulk point lookups).
223 pub fn set_order_by(mut self, order_by: OrderBy) -> Self {
224 self.order_by = Some(order_by);
225 self
226 }
227
228 /// Sets the lock hint for this read.
229 ///
230 /// # Example
231 /// ```
232 /// # use google_cloud_spanner::read::ReadRequest;
233 /// # use google_cloud_spanner::key::KeySet;
234 /// # use google_cloud_spanner::model::read_request::LockHint;
235 /// let request = ReadRequest::builder("Users", vec!["Id"])
236 /// .with_keys(KeySet::all())
237 /// .set_lock_hint(LockHint::Exclusive);
238 /// ```
239 ///
240 /// Lock hints can only be used with read-write transactions.
241 ///
242 /// By default, Spanner acquires shared read locks, which allows other reads to
243 /// still access the data until your transaction is ready to commit.
244 ///
245 /// Requesting exclusive locks (`LOCK_HINT_EXCLUSIVE`) is beneficial if you observe
246 /// high write contention. It prevents deadlocks by avoiding the situation where
247 /// multiple transactions initially acquire shared locks and then both try to upgrade
248 /// to exclusive locks at the same time.
249 ///
250 /// Request exclusive locks judiciously because they block others from reading that
251 /// data for the entire transaction, rather than just when the writes are being performed.
252 pub fn set_lock_hint(mut self, lock_hint: LockHint) -> Self {
253 self.lock_hint = Some(lock_hint);
254 self
255 }
256
257 /// Sets the per-attempt timeout for this read request.
258 pub fn with_attempt_timeout(mut self, timeout: Duration) -> Self {
259 self.gax_options.set_attempt_timeout(timeout);
260 self
261 }
262
263 /// Sets the retry policy for this read request.
264 pub fn with_retry_policy(mut self, policy: impl Into<RetryPolicyArg>) -> Self {
265 self.gax_options.set_retry_policy(policy);
266 self
267 }
268
269 /// Sets the backoff policy for this read request.
270 pub fn with_backoff_policy(mut self, policy: impl Into<BackoffPolicyArg>) -> Self {
271 self.gax_options.set_backoff_policy(policy);
272 self
273 }
274
275 /// Builds the configured `ReadRequest`.
276 pub fn build(self) -> ReadRequest {
277 ReadRequest {
278 table: self.table,
279 index: self.index,
280 keys: self.keys,
281 columns: self.columns,
282 limit: self.limit,
283 request_options: self.request_options,
284 directed_read_options: self.directed_read_options,
285 order_by: self.order_by,
286 lock_hint: self.lock_hint,
287 gax_options: self.gax_options,
288 }
289 }
290}
291
292/// Represents a configured read request ready for execution.
293///
294/// Contains the table, optional index, keys, and columns.
295/// Allows configuring optional parameters on the read operation, such as a limit.
296#[derive(Clone, Debug)]
297pub struct ReadRequest {
298 pub(crate) table: String,
299 pub(crate) index: Option<String>,
300 pub(crate) keys: KeySet,
301 pub(crate) columns: Vec<String>,
302 pub(crate) limit: Option<i64>,
303 pub(crate) request_options: Option<crate::model::RequestOptions>,
304 pub(crate) directed_read_options: Option<DirectedReadOptions>,
305 pub(crate) order_by: Option<OrderBy>,
306 pub(crate) lock_hint: Option<LockHint>,
307 pub(crate) gax_options: GaxRequestOptions,
308}
309
310impl ReadRequest {
311 /// Creates a new read operation builder for a specific table.
312 ///
313 /// # Example
314 /// ```
315 /// # use google_cloud_spanner::read::ReadRequest;
316 /// let builder = ReadRequest::builder("Users", vec!["Id", "Name"]);
317 /// ```
318 ///
319 /// The table name and columns to retrieve are required to initiate a read.
320 pub fn builder(
321 table: impl Into<String>,
322 columns: impl IntoIterator<Item = impl Into<String>>,
323 ) -> ReadRequestBuilder {
324 ReadRequestBuilder {
325 table: table.into(),
326 columns: columns.into_iter().map(|s| s.into()).collect(),
327 }
328 }
329
330 pub(crate) fn into_request(self) -> crate::model::ReadRequest {
331 crate::model::ReadRequest::default()
332 .set_table(self.table)
333 .set_columns(self.columns)
334 .set_key_set(self.keys.into_proto())
335 .set_index(self.index.unwrap_or_default())
336 .set_limit(self.limit.unwrap_or_default())
337 .set_or_clear_request_options(self.request_options)
338 .set_or_clear_directed_read_options(self.directed_read_options)
339 .set_order_by(self.order_by.unwrap_or_default())
340 .set_lock_hint(self.lock_hint.unwrap_or_default())
341 }
342
343 pub(crate) fn into_partition_read_request(self) -> crate::model::PartitionReadRequest {
344 crate::model::PartitionReadRequest::default()
345 .set_table(self.table)
346 .set_columns(self.columns)
347 .set_key_set(self.keys.into_proto())
348 .set_index(self.index.unwrap_or_default())
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn auto_traits() {
358 static_assertions::assert_impl_all!(ReadRequestBuilder: Send, Sync, Clone, std::fmt::Debug);
359 static_assertions::assert_impl_all!(ConfiguredReadRequestBuilder: Send, Sync, Clone, std::fmt::Debug);
360 static_assertions::assert_impl_all!(ReadRequest: Send, Sync, Clone, std::fmt::Debug);
361 }
362
363 #[test]
364 fn read_with_keys() {
365 let keys = KeySet::all();
366 let req = ReadRequest::builder("MyTable", vec!["col1", "col2"])
367 .with_keys(keys.clone())
368 .build();
369 assert_eq!(req.table, "MyTable");
370 assert_eq!(req.index, None);
371 assert_eq!(req.keys, keys);
372 assert_eq!(req.columns, vec!["col1", "col2"]);
373 assert_eq!(req.limit, None);
374 }
375
376 #[test]
377 fn read_with_index() {
378 let keys = KeySet::all();
379 let req = ReadRequest::builder("MyTable", vec!["col1", "col2"])
380 .with_index("MyIndex", keys.clone())
381 .build();
382 assert_eq!(req.table, "MyTable");
383 assert_eq!(req.index, Some("MyIndex".to_string()));
384 assert_eq!(req.keys, keys);
385 assert_eq!(req.columns, vec!["col1", "col2"]);
386 assert_eq!(req.limit, None);
387 }
388
389 #[test]
390 fn with_limit() {
391 let req = ReadRequest::builder("MyTable", vec!["col1"])
392 .with_keys(KeySet::all())
393 .set_limit(42)
394 .build();
395 assert_eq!(req.limit, Some(42));
396 }
397
398 #[test]
399 fn with_request_tag() {
400 let req = ReadRequest::builder("MyTable", vec!["col1"])
401 .with_keys(KeySet::all())
402 .set_request_tag("tag1")
403 .build();
404 assert_eq!(
405 req.request_options
406 .expect("request options missing")
407 .request_tag,
408 "tag1"
409 );
410 }
411
412 #[test]
413 fn with_priority() {
414 let req = ReadRequest::builder("MyTable", vec!["col1"])
415 .with_keys(KeySet::all())
416 .set_priority(Priority::High)
417 .build();
418 assert_eq!(
419 req.request_options
420 .expect("request options missing")
421 .priority,
422 Priority::High
423 );
424 }
425
426 #[test]
427 fn with_directed_read_options() {
428 let dro = DirectedReadOptions::default();
429 let req = ReadRequest::builder("MyTable", vec!["col1"])
430 .with_keys(KeySet::all())
431 .set_directed_read_options(dro.clone())
432 .build();
433 assert_eq!(req.directed_read_options, Some(dro));
434 }
435
436 #[test]
437 fn with_order_by() {
438 let req = ReadRequest::builder("MyTable", vec!["col1"])
439 .with_keys(KeySet::all())
440 .set_order_by(OrderBy::PrimaryKey)
441 .build();
442 assert_eq!(req.order_by, Some(OrderBy::PrimaryKey));
443 }
444
445 #[test]
446 fn with_lock_hint() {
447 let req = ReadRequest::builder("MyTable", vec!["col1"])
448 .with_keys(KeySet::all())
449 .set_lock_hint(LockHint::Exclusive)
450 .build();
451 assert_eq!(req.lock_hint, Some(LockHint::Exclusive));
452 }
453
454 #[test]
455 fn with_gax_options() -> anyhow::Result<()> {
456 use google_cloud_gax::exponential_backoff::ExponentialBackoff;
457 use google_cloud_gax::retry_policy::NeverRetry;
458 use std::time::Duration;
459
460 let req = ReadRequest::builder("MyTable", vec!["col1"])
461 .with_keys(KeySet::all())
462 .with_attempt_timeout(Duration::from_secs(10))
463 .with_retry_policy(NeverRetry)
464 .with_backoff_policy(ExponentialBackoff::default())
465 .build();
466
467 assert_eq!(
468 req.gax_options.attempt_timeout(),
469 &Some(Duration::from_secs(10))
470 );
471 assert!(req.gax_options.retry_policy().is_some());
472 assert!(req.gax_options.backoff_policy().is_some());
473
474 Ok(())
475 }
476}