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