@@ -6,9 +6,11 @@ use serial_test::serial;
66use time:: { Duration , OffsetDateTime } ;
77
88use common:: * ;
9+ use google_cloud_googleapis:: spanner:: v1:: execute_sql_request:: QueryMode ;
910use google_cloud_spanner:: key:: Key ;
1011use google_cloud_spanner:: row:: Row ;
1112use google_cloud_spanner:: statement:: Statement ;
13+ use google_cloud_spanner:: transaction:: QueryOptions ;
1214use google_cloud_spanner:: transaction_ro:: ReadOnlyTransaction ;
1315
1416mod 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