google_cloud_bigquery/query/
iterator.rs1use crate::error::RowError;
16use crate::query::{CompleteQuery, Row, Schema};
17use google_cloud_bigquery_v2::client::JobService;
18use google_cloud_bigquery_v2::model::{GetQueryResultsRequest, JobReference};
19use std::collections::VecDeque;
20use std::sync::Arc;
21
22pub type Result<T> = std::result::Result<T, RowError>;
23#[derive(Debug)]
51pub struct RowIterator {
52 job_service: Arc<JobService>,
53 job_ref: Option<JobReference>,
54 schema: Arc<Schema>,
55 page_token: Option<String>,
56 rows: VecDeque<wkt::Struct>,
57 max_results: Option<u32>,
58}
59
60impl RowIterator {
61 pub(crate) fn new(q: CompleteQuery) -> Self {
62 Self {
63 job_service: q.job_service,
64 job_ref: q.job_ref,
65 schema: q.schema,
66 page_token: q.page_token,
67 rows: q.cached_rows,
68 max_results: q.max_results,
69 }
70 }
71
72 pub fn set_max_results(mut self, max_results: u32) -> Self {
92 self.max_results = Some(max_results);
93 self
94 }
95
96 pub async fn next(&mut self) -> Option<Result<Row>> {
113 loop {
114 if let Some(raw_row) = self.rows.pop_front() {
115 return Some(Row::try_new(raw_row, &self.schema));
116 }
117
118 if let Err(e) = self.try_fetch_page().await {
119 return Some(Err(e));
120 }
121
122 if self.rows.is_empty() && self.page_token.is_none() {
123 return None;
124 }
125 }
126 }
127
128 async fn try_fetch_page(&mut self) -> Result<()> {
129 let Some(token) = self.page_token.as_deref() else {
130 return Ok(());
131 };
132
133 let (fetched_rows, next_token) = self.fetch_page(token).await?;
134 self.page_token = next_token;
135 self.rows.extend(fetched_rows);
136 Ok(())
137 }
138
139 async fn fetch_page(&self, token: &str) -> Result<(Vec<wkt::Struct>, Option<String>)> {
141 let job_ref = self.job_ref.as_ref().expect(
142 "only queries with a job reference should have page tokens and can fetch more pages",
143 );
144
145 let mut req = GetQueryResultsRequest::new()
146 .set_project_id(job_ref.project_id.clone())
147 .set_or_clear_max_results(self.max_results)
148 .set_job_id(job_ref.job_id.clone())
149 .set_page_token(token)
150 .set_format_options(
151 google_cloud_bigquery_v2::model::DataFormatOptions::new()
152 .set_use_int64_timestamp(true),
153 );
154 if let Some(location) = job_ref.location.clone() {
155 req = req.set_location(location);
156 }
157
158 let res = self
159 .job_service
160 .get_query_results()
161 .with_request(req)
162 .send()
163 .await?;
164
165 let page_token = if res.page_token.is_empty() {
166 None
167 } else {
168 Some(res.page_token)
169 };
170
171 Ok((res.rows, page_token))
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::query::tests::{MockJobService, create_job_service};
179 use google_cloud_bigquery_v2::model::{
180 DataFormatOptions, GetQueryResultsResponse, JobReference, QueryResponse, TableFieldSchema,
181 TableSchema,
182 };
183 use google_cloud_gax::error::Error as GaxError;
184 use google_cloud_gax::error::rpc::{Code, Status};
185 use google_cloud_gax::response::Response;
186 use serde_json::{Map, json};
187 use std::sync::Arc;
188
189 type TestResult = anyhow::Result<()>;
190
191 fn create_test_schema() -> TableSchema {
192 TableSchema::new().set_fields([TableFieldSchema::new()
193 .set_name("col")
194 .set_type("STRING")
195 .set_mode("NULLABLE")])
196 }
197
198 fn create_test_row(val: &str) -> wkt::Struct {
199 Map::from_iter([("f".to_string(), json!([{ "v": val }]))])
200 }
201
202 fn create_test_job_ref() -> JobReference {
203 JobReference::new()
204 .set_project_id("test_project")
205 .set_job_id("test_job")
206 }
207
208 fn create_test_job_ref_with_location(location: &str) -> JobReference {
209 create_test_job_ref().set_location(location)
210 }
211
212 fn create_test_complete_query(
213 job_service: Arc<JobService>,
214 job_ref: Option<JobReference>,
215 rows: Vec<wkt::Struct>,
216 page_token: Option<String>,
217 ) -> CompleteQuery {
218 let mut res = QueryResponse::new()
219 .set_schema(create_test_schema())
220 .set_rows(rows);
221 if let Some(token) = page_token {
222 res = res.set_page_token(token);
223 }
224 if let Some(job_ref) = job_ref {
225 res = res.set_job_reference(job_ref);
226 }
227 CompleteQuery::from_query_response(job_service, res, None)
228 }
229
230 #[tokio::test]
231 async fn test_row_iterator_empty_no_token() -> TestResult {
232 let job_service = create_job_service(MockJobService::new());
233 let q = create_test_complete_query(job_service, Some(create_test_job_ref()), vec![], None);
234 let mut iter = q.read();
235 assert!(iter.next().await.is_none(), "{iter:?}");
236 Ok(())
237 }
238
239 #[tokio::test]
240 async fn test_row_iterator_cached_rows_only() -> TestResult {
241 let job_service = create_job_service(MockJobService::new());
242 let rows = vec![create_test_row("first"), create_test_row("second")];
243 let q = create_test_complete_query(job_service, Some(create_test_job_ref()), rows, None);
244 let mut iter = q.read();
245
246 let row1 = iter.next().await.expect("should have row 1")?;
247 assert_eq!(row1.get::<String, _>("col"), "first");
248
249 let row2 = iter.next().await.expect("should have row 2")?;
250 assert_eq!(row2.get::<String, _>("col"), "second");
251
252 assert!(iter.next().await.is_none(), "{iter:?}");
253 Ok(())
254 }
255
256 #[tokio::test]
257 async fn test_row_iterator_row_conversion_error() -> TestResult {
258 let job_service = create_job_service(MockJobService::new());
259 let invalid_row = Map::from_iter([("f".to_string(), json!([]))]);
260 let q = create_test_complete_query(
261 job_service,
262 Some(create_test_job_ref()),
263 vec![invalid_row],
264 None,
265 );
266 let mut iter = q.read();
267
268 let err = iter.next().await.expect("should return error").unwrap_err();
269 assert!(matches!(err, RowError::InvalidRowFormat(_)), "{err:?}");
270 assert!(iter.next().await.is_none(), "{iter:?}");
271 Ok(())
272 }
273
274 #[tokio::test]
275 async fn test_row_iterator_fetch_multiple_pages() -> TestResult {
276 let mut mock = MockJobService::new();
277 let mut seq = mockall::Sequence::new();
278
279 mock.expect_get_query_results()
280 .in_sequence(&mut seq)
281 .times(1)
282 .returning(|req, _| {
283 assert_eq!(req.page_token, "token_page_1");
284 let res = GetQueryResultsResponse::new()
285 .set_rows(vec![
286 create_test_row("page_1_row_1"),
287 create_test_row("page_1_row_2"),
288 ])
289 .set_page_token("token_page_2");
290 Ok(Response::from(res))
291 });
292
293 mock.expect_get_query_results()
294 .in_sequence(&mut seq)
295 .times(1)
296 .returning(|req, _| {
297 assert_eq!(req.page_token, "token_page_2");
298 let res = GetQueryResultsResponse::new()
299 .set_rows(vec![create_test_row("page_2_row_1")])
300 .set_page_token("");
301 Ok(Response::from(res))
302 });
303
304 let job_service = create_job_service(mock);
305 let q = create_test_complete_query(
306 job_service,
307 Some(create_test_job_ref()),
308 vec![create_test_row("cached_row")],
309 Some("token_page_1".to_string()),
310 );
311 let mut iter = q.read();
312
313 let row1 = iter.next().await.expect("should have row 1")?;
314 assert_eq!(row1.get::<String, _>("col"), "cached_row");
315
316 let row2 = iter.next().await.expect("should have row 2")?;
317 assert_eq!(row2.get::<String, _>("col"), "page_1_row_1");
318
319 let row3 = iter.next().await.expect("should have row 3")?;
320 assert_eq!(row3.get::<String, _>("col"), "page_1_row_2");
321
322 let row4 = iter.next().await.expect("should have row 4")?;
323 assert_eq!(row4.get::<String, _>("col"), "page_2_row_1");
324
325 assert!(iter.next().await.is_none(), "{iter:?}");
326 Ok(())
327 }
328
329 #[tokio::test]
330 async fn test_row_iterator_fetch_page_request_parameters() -> TestResult {
331 let mut mock = MockJobService::new();
332 mock.expect_get_query_results()
333 .times(1)
334 .returning(|req, _| {
335 assert_eq!(req.project_id, "test_project");
336 assert_eq!(req.job_id, "test_job");
337 assert_eq!(req.page_token, "token_1");
338 assert_eq!(req.max_results, Some(50));
339 assert_eq!(req.location, "us-east1");
340 assert_eq!(
341 req.format_options,
342 Some(DataFormatOptions::new().set_use_int64_timestamp(true))
343 );
344 let res = GetQueryResultsResponse::new()
345 .set_rows(vec![create_test_row("page_row")])
346 .set_page_token("");
347 Ok(Response::from(res))
348 });
349
350 let job_service = create_job_service(mock);
351 let q = create_test_complete_query(
352 job_service,
353 Some(create_test_job_ref_with_location("us-east1")),
354 vec![],
355 Some("token_1".to_string()),
356 );
357 let mut iter = q.read().set_max_results(50);
358
359 let row = iter.next().await.expect("should have row")?;
360 assert_eq!(row.get::<String, _>("col"), "page_row");
361
362 assert!(iter.next().await.is_none(), "{iter:?}");
363 Ok(())
364 }
365
366 #[tokio::test]
367 async fn test_row_iterator_inherits_max_results() -> TestResult {
368 let mut mock = MockJobService::new();
369 mock.expect_get_query_results()
370 .times(1)
371 .returning(|req, _| {
372 assert_eq!(req.max_results, Some(25));
373 let res = GetQueryResultsResponse::new()
374 .set_rows(vec![create_test_row("page_row")])
375 .set_page_token("");
376 Ok(Response::from(res))
377 });
378
379 let job_service = create_job_service(mock);
380 let res = QueryResponse::new()
381 .set_schema(create_test_schema())
382 .set_job_reference(create_test_job_ref())
383 .set_page_token("token_1");
384 let q = CompleteQuery::from_query_response(job_service, res, Some(25));
385 let mut iter = q.read();
386
387 let row = iter.next().await.expect("should have row")?;
388 assert_eq!(row.get::<String, _>("col"), "page_row");
389 assert!(iter.next().await.is_none(), "{iter:?}");
390 Ok(())
391 }
392
393 #[tokio::test]
394 async fn test_row_iterator_fetch_page_rpc_error() -> TestResult {
395 let mut mock = MockJobService::new();
396 mock.expect_get_query_results().returning(|_, _| {
397 let status = Status::default()
398 .set_code(Code::Unavailable)
399 .set_message("temporary service error");
400 Err(GaxError::service(status))
401 });
402
403 let job_service = create_job_service(mock);
404 let q = create_test_complete_query(
405 job_service,
406 Some(create_test_job_ref()),
407 vec![],
408 Some("token_err".to_string()),
409 );
410 let mut iter = q.read();
411
412 let err = iter.next().await.expect("should return error").unwrap_err();
413 assert!(matches!(err, RowError::Rpc { .. }), "{err:?}");
414 assert!(
415 err.to_string().contains("temporary service error"),
416 "{err:?}"
417 );
418 Ok(())
419 }
420}