Skip to content

Commit 6fbb5de

Browse files
authored
fix(spanner): process metadata and stats for PartialResultSets with empty values (#428)
1 parent 52f2395 commit 6fbb5de

2 files changed

Lines changed: 89 additions & 3 deletions

File tree

spanner/src/reader.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -331,9 +331,6 @@ where
331331
async fn try_recv(&mut self, option: Option<CallOptions>) -> Result<bool, Status> {
332332
loop {
333333
if let Some(result_set) = self.prs_buffer.pop_ready(self.end_of_stream) {
334-
if result_set.values.is_empty() {
335-
return Ok(false);
336-
}
337334
let resume_token_present = !result_set.resume_token.is_empty();
338335
//if resume_token changes set new resume_token
339336
if resume_token_present {
@@ -343,6 +340,12 @@ where
343340
if result_set.stats.is_some() {
344341
self.stats = result_set.stats;
345342
}
343+
if result_set.values.is_empty() {
344+
// Process metadata even when values are empty (e.g., QueryMode::Plan)
345+
self.rs
346+
.add(result_set.metadata, result_set.values, result_set.chunked_value)?;
347+
return Ok(false);
348+
}
346349
let added = self
347350
.rs
348351
.add(result_set.metadata, result_set.values, result_set.chunked_value)?;

spanner/tests/transaction_ro_test.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ use serial_test::serial;
66
use time::{Duration, OffsetDateTime};
77

88
use common::*;
9+
use google_cloud_googleapis::spanner::v1::execute_sql_request::QueryMode;
910
use google_cloud_spanner::key::Key;
1011
use google_cloud_spanner::row::Row;
1112
use google_cloud_spanner::statement::Statement;
13+
use google_cloud_spanner::transaction::QueryOptions;
1214
use google_cloud_spanner::transaction_ro::ReadOnlyTransaction;
1315

1416
mod common;
@@ -383,3 +385,84 @@ async fn test_big_decimal() {
383385
row.column::<BigDecimal>(6).unwrap().to_string()
384386
);
385387
}
388+
389+
#[tokio::test]
390+
#[serial]
391+
async fn test_query_plan_mode_metadata() {
392+
let data_client = create_data_client().await;
393+
let mut tx = data_client.read_only_transaction().await.unwrap();
394+
395+
let stmt = Statement::new("SELECT UserId, NotNullINT64 FROM User LIMIT 1");
396+
let options = QueryOptions {
397+
mode: QueryMode::Plan,
398+
..Default::default()
399+
};
400+
let mut iter = tx.query_with_option(stmt, options).await.unwrap();
401+
402+
// Drain iterator — Plan mode returns no data rows, so next() returns None immediately.
403+
// Metadata is populated via try_recv() during this call.
404+
assert!(iter.next().await.unwrap().is_none());
405+
406+
// Verify metadata is populated after draining despite no data rows
407+
let metadata = iter.columns_metadata();
408+
assert!(!metadata.is_empty(), "Plan mode should populate column metadata");
409+
assert_eq!(metadata.len(), 2);
410+
assert_eq!(metadata[0].name, "UserId");
411+
assert_eq!(metadata[1].name, "NotNullINT64");
412+
413+
// Note: On real Spanner, Plan mode also returns stats with query_plan
414+
// (plan_nodes). The emulator does not return stats for Plan mode, so we
415+
// cannot assert on query_plan here. Verified manually against a real instance
416+
// that stats().query_plan is Some with populated plan_nodes.
417+
}
418+
419+
#[tokio::test]
420+
#[serial]
421+
async fn test_query_empty_result_metadata() {
422+
let data_client = create_data_client().await;
423+
let mut tx = data_client.read_only_transaction().await.unwrap();
424+
425+
// Query that returns 0 rows but should still have metadata
426+
let stmt = Statement::new("SELECT UserId, NotNullINT64 FROM User WHERE FALSE");
427+
let mut iter = tx.query(stmt).await.unwrap();
428+
429+
// No rows returned; metadata is populated via try_recv() during this call.
430+
assert!(iter.next().await.unwrap().is_none());
431+
432+
// Metadata should still be populated after draining
433+
let metadata = iter.columns_metadata();
434+
assert!(!metadata.is_empty(), "Empty result should still populate column metadata");
435+
assert_eq!(metadata.len(), 2);
436+
assert_eq!(metadata[0].name, "UserId");
437+
assert_eq!(metadata[1].name, "NotNullINT64");
438+
}
439+
440+
#[tokio::test]
441+
#[serial]
442+
async fn test_query_profile_mode_empty_result_stats() {
443+
let data_client = create_data_client().await;
444+
let mut tx = data_client.read_only_transaction().await.unwrap();
445+
446+
// Profile mode with 0 rows: values are empty, but stats should still be captured
447+
let stmt = Statement::new("SELECT UserId, NotNullINT64 FROM User WHERE FALSE");
448+
let options = QueryOptions {
449+
mode: QueryMode::Profile,
450+
..Default::default()
451+
};
452+
let mut iter = tx.query_with_option(stmt, options).await.unwrap();
453+
454+
// Drain iterator — no data rows returned.
455+
assert!(iter.next().await.unwrap().is_none());
456+
457+
// Metadata should be populated after draining
458+
let metadata = iter.columns_metadata();
459+
assert!(!metadata.is_empty(), "Profile mode empty result should populate column metadata");
460+
assert_eq!(metadata.len(), 2);
461+
assert_eq!(metadata[0].name, "UserId");
462+
assert_eq!(metadata[1].name, "NotNullINT64");
463+
464+
// Stats (query_stats) should be available even with 0 rows
465+
let stats = iter.stats();
466+
assert!(stats.is_some(), "Profile mode should return stats even with empty results");
467+
assert!(stats.unwrap().query_stats.is_some(), "Profile mode should return query_stats");
468+
}

0 commit comments

Comments
 (0)