test: add more tests
diff --git a/spanner/request_id_header_test.go b/spanner/request_id_header_test.go
index e49d37d..4aecb94 100644
--- a/spanner/request_id_header_test.go
+++ b/spanner/request_id_header_test.go
@@ -20,6 +20,7 @@
 	"fmt"
 	"math"
 	"regexp"
+	"slices"
 	"strings"
 	"sync"
 	"sync/atomic"
@@ -1166,7 +1167,7 @@
 	}
 }
 
-func TestRequestIDHeader_multipleParallelCallsWithConventialCustomerCalls(t *testing.T) {
+func TestRequestIDHeader_multipleParallelCallsWithConventionalCustomerCalls(t *testing.T) {
 	t.Parallel()
 
 	interceptorTracker := newInterceptorTracker()
@@ -1287,6 +1288,11 @@
 	return st.Err()
 }
 
+func newInvalidArgumentError() error {
+	st := status.New(codes.InvalidArgument, "Invalid argument")
+	return st.Err()
+}
+
 func TestRequestIDHeader_RetryOnAbortAndValidate(t *testing.T) {
 	t.Parallel()
 
@@ -1371,6 +1377,380 @@
 	}
 }
 
+func TestRequestIDHeader_BatchCreateSessions_Unavailable(t *testing.T) {
+	t.Parallel()
+
+	ctx := context.Background()
+	interceptorTracker := newInterceptorTracker()
+	clientOpts := []option.ClientOption{
+		option.WithGRPCDialOption(grpc.WithUnaryInterceptor(interceptorTracker.unaryClientInterceptor)),
+		option.WithGRPCDialOption(grpc.WithStreamInterceptor(interceptorTracker.streamClientInterceptor)),
+	}
+	clientConfig := ClientConfig{
+		SessionPoolConfig: SessionPoolConfig{
+			MinOpened:     2,
+			MaxOpened:     10,
+			WriteSessions: 0.2,
+			incStep:       2,
+		},
+	}
+
+	server, sc, tearDown := setupMockedTestServerWithConfigAndClientOptions(t, clientConfig, clientOpts)
+	t.Cleanup(tearDown)
+	defer sc.Close()
+
+	// BatchCreateSessions returns UNAVAILABLE and should be retried.
+	server.TestSpanner.PutExecutionTime(testutil.MethodBatchCreateSession,
+		testutil.SimulatedExecutionTime{
+			Errors: []error{
+				newUnavailableErrorWithMinimalRetryDelay(),
+			},
+		})
+	iter := sc.Single().Query(ctx, Statement{SQL: testutil.SelectFooFromBar})
+	defer iter.Stop()
+	for {
+		_, err := iter.Next()
+		if err == iterator.Done {
+			break
+		}
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	if _, err := shouldHaveReceived(server.TestSpanner, []interface{}{
+		&sppb.BatchCreateSessionsRequest{},
+		&sppb.BatchCreateSessionsRequest{},
+		&sppb.ExecuteSqlRequest{},
+	}); err != nil {
+		t.Fatal(err)
+	}
+
+	if g, w := interceptorTracker.unaryCallCount(), uint64(2); g != w {
+		t.Errorf("unaryClientCall is incorrect; got=%d want=%d", g, w)
+	}
+
+	if g, w := interceptorTracker.streamCallCount(), uint64(1); g != w {
+		t.Errorf("streamClientCall is incorrect; got=%d want=%d", g, w)
+	}
+
+	if err := interceptorTracker.validateRequestIDsMonotonicity(); err != nil {
+		t.Fatal(err)
+	}
+
+	clientID := uint32(sc.sc.nthClient)
+	procID := randIDForProcess
+	version := xSpannerRequestIDVersion
+	wantUnarySegments := []*requestIDSegments{
+		{Version: version, ProcessID: procID, ClientID: clientID, ChannelID: 1, RequestNo: 1, RPCNo: 1}, // BatchCreateSession (initial attempt)
+		{Version: version, ProcessID: procID, ClientID: clientID, ChannelID: 1, RequestNo: 1, RPCNo: 2}, // BatchCreateSession (retry)
+	}
+	wantStreamingSegments := []*requestIDSegments{
+		{Version: version, ProcessID: procID, ClientID: clientID, ChannelID: 1, RequestNo: 2, RPCNo: 1}, // ExecuteStreamingSql
+	}
+
+	if diff := cmp.Diff(interceptorTracker.unaryClientRequestIDSegments, wantUnarySegments); diff != "" {
+		t.Fatalf("RequestID unary segments mismatch: got - want +\n%s", diff)
+	}
+	if diff := cmp.Diff(interceptorTracker.streamClientRequestIDSegments, wantStreamingSegments); diff != "" {
+		t.Fatalf("RequestID streaming segments mismatch: got - want +\n%s", diff)
+	}
+}
+
+func TestRequestIDHeader_SingleUseReadOnly_ExecuteStreamingSql_Unavailable(t *testing.T) {
+	t.Parallel()
+
+	ctx := context.Background()
+	interceptorTracker := newInterceptorTracker()
+	clientOpts := []option.ClientOption{
+		option.WithGRPCDialOption(grpc.WithUnaryInterceptor(interceptorTracker.unaryClientInterceptor)),
+		option.WithGRPCDialOption(grpc.WithStreamInterceptor(interceptorTracker.streamClientInterceptor)),
+	}
+	clientConfig := ClientConfig{
+		SessionPoolConfig: SessionPoolConfig{
+			MinOpened:     2,
+			MaxOpened:     10,
+			WriteSessions: 0.2,
+			incStep:       2,
+		},
+	}
+
+	server, sc, tearDown := setupMockedTestServerWithConfigAndClientOptions(t, clientConfig, clientOpts)
+	t.Cleanup(tearDown)
+	defer sc.Close()
+
+	// ExecuteStreamingSql returns UNAVAILABLE and should be retried.
+	server.TestSpanner.PutExecutionTime(testutil.MethodExecuteStreamingSql,
+		testutil.SimulatedExecutionTime{
+			Errors: []error{
+				newUnavailableErrorWithMinimalRetryDelay(),
+			},
+		})
+	iter := sc.Single().Query(ctx, Statement{SQL: testutil.SelectFooFromBar})
+	defer iter.Stop()
+	for {
+		_, err := iter.Next()
+		if err == iterator.Done {
+			break
+		}
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	if _, err := shouldHaveReceived(server.TestSpanner, []interface{}{
+		&sppb.BatchCreateSessionsRequest{},
+		&sppb.ExecuteSqlRequest{},
+		&sppb.ExecuteSqlRequest{},
+	}); err != nil {
+		t.Fatal(err)
+	}
+
+	if g, w := interceptorTracker.unaryCallCount(), uint64(1); g != w {
+		t.Errorf("unaryClientCall is incorrect; got=%d want=%d", g, w)
+	}
+
+	if g, w := interceptorTracker.streamCallCount(), uint64(2); g != w {
+		t.Errorf("streamClientCall is incorrect; got=%d want=%d", g, w)
+	}
+
+	if err := interceptorTracker.validateRequestIDsMonotonicity(); err != nil {
+		t.Fatal(err)
+	}
+
+	clientID := uint32(sc.sc.nthClient)
+	procID := randIDForProcess
+	version := xSpannerRequestIDVersion
+	wantUnarySegments := []*requestIDSegments{
+		{Version: version, ProcessID: procID, ClientID: clientID, ChannelID: 1, RequestNo: 1, RPCNo: 1}, // BatchCreateSession
+	}
+	wantStreamingSegments := []*requestIDSegments{
+		{Version: version, ProcessID: procID, ClientID: clientID, ChannelID: 1, RequestNo: 2, RPCNo: 1}, // ExecuteStreamingSql (initial attempt)
+		{Version: version, ProcessID: procID, ClientID: clientID, ChannelID: 1, RequestNo: 2, RPCNo: 2}, // ExecuteStreamingSql (retry)
+	}
+
+	if diff := cmp.Diff(interceptorTracker.unaryClientRequestIDSegments, wantUnarySegments); diff != "" {
+		t.Fatalf("RequestID unary segments mismatch: got - want +\n%s", diff)
+	}
+	if diff := cmp.Diff(interceptorTracker.streamClientRequestIDSegments, wantStreamingSegments); diff != "" {
+		t.Fatalf("RequestID streaming segments mismatch: got - want +\n%s", diff)
+	}
+}
+
+func TestRequestIDHeader_SingleUseReadOnly_ExecuteStreamingSql_InvalidArgument(t *testing.T) {
+	t.Parallel()
+
+	ctx := context.Background()
+	interceptorTracker := newInterceptorTracker()
+	clientOpts := []option.ClientOption{
+		option.WithGRPCDialOption(grpc.WithUnaryInterceptor(interceptorTracker.unaryClientInterceptor)),
+		option.WithGRPCDialOption(grpc.WithStreamInterceptor(interceptorTracker.streamClientInterceptor)),
+	}
+	clientConfig := ClientConfig{
+		SessionPoolConfig: SessionPoolConfig{
+			MinOpened:     2,
+			MaxOpened:     10,
+			WriteSessions: 0.2,
+			incStep:       2,
+		},
+	}
+
+	server, sc, tearDown := setupMockedTestServerWithConfigAndClientOptions(t, clientConfig, clientOpts)
+	t.Cleanup(tearDown)
+	defer sc.Close()
+
+	// Simulate that ExecuteStreamingSql is slow.
+	server.TestSpanner.PutExecutionTime(testutil.MethodExecuteStreamingSql,
+		testutil.SimulatedExecutionTime{Errors: []error{newInvalidArgumentError()}})
+
+	iter := sc.Single().Query(ctx, Statement{SQL: testutil.SelectFooFromBar})
+	defer iter.Stop()
+	_, err := iter.Next()
+	if err == nil {
+		t.Fatal("missing invalid argument error")
+	}
+	if g, w := ErrCode(err), codes.InvalidArgument; g != w {
+		t.Fatalf("error code mismatch\n Got: %v\nWant: %v", g, w)
+	}
+	spannerError, ok := err.(*Error)
+	if !ok {
+		t.Fatal("not a Spanner error")
+	}
+	if spannerError.RequestID == "" {
+		t.Fatal("missing RequestID on error")
+	}
+}
+
+func TestRequestIDHeader_SingleUseReadOnly_ExecuteStreamingSql_ContextDeadlineExceeded(t *testing.T) {
+	t.Parallel()
+
+	ctx := context.Background()
+	interceptorTracker := newInterceptorTracker()
+	clientOpts := []option.ClientOption{
+		option.WithGRPCDialOption(grpc.WithUnaryInterceptor(interceptorTracker.unaryClientInterceptor)),
+		option.WithGRPCDialOption(grpc.WithStreamInterceptor(interceptorTracker.streamClientInterceptor)),
+	}
+	clientConfig := ClientConfig{
+		SessionPoolConfig: SessionPoolConfig{
+			MinOpened:     2,
+			MaxOpened:     10,
+			WriteSessions: 0.2,
+			incStep:       2,
+		},
+	}
+
+	server, sc, tearDown := setupMockedTestServerWithConfigAndClientOptions(t, clientConfig, clientOpts)
+	t.Cleanup(tearDown)
+	defer sc.Close()
+
+	// Simulate that ExecuteStreamingSql is slow.
+	server.TestSpanner.PutExecutionTime(testutil.MethodExecuteStreamingSql,
+		testutil.SimulatedExecutionTime{MinimumExecutionTime: time.Second})
+	ctx, cancel := context.WithTimeout(ctx, 10*time.Millisecond)
+	defer cancel()
+	iter := sc.Single().Query(ctx, Statement{SQL: testutil.SelectFooFromBar})
+	defer iter.Stop()
+	_, err := iter.Next()
+	if err == nil {
+		t.Fatal("missing deadline exceeded error")
+	}
+	if g, w := ErrCode(err), codes.DeadlineExceeded; g != w {
+		t.Fatalf("error code mismatch\n Got: %v\nWant: %v", g, w)
+	}
+	spannerError, ok := err.(*Error)
+	if !ok {
+		t.Fatal("not a Spanner error")
+	}
+	if spannerError.RequestID == "" {
+		t.Fatal("missing RequestID on error")
+	}
+}
+
+func TestRequestIDHeader_Commit_ContextDeadlineExceeded(t *testing.T) {
+	t.Parallel()
+
+	ctx := context.Background()
+	interceptorTracker := newInterceptorTracker()
+	clientOpts := []option.ClientOption{
+		option.WithGRPCDialOption(grpc.WithUnaryInterceptor(interceptorTracker.unaryClientInterceptor)),
+		option.WithGRPCDialOption(grpc.WithStreamInterceptor(interceptorTracker.streamClientInterceptor)),
+	}
+	clientConfig := ClientConfig{
+		SessionPoolConfig: SessionPoolConfig{
+			MinOpened:     2,
+			MaxOpened:     10,
+			WriteSessions: 0.2,
+			incStep:       2,
+		},
+	}
+
+	server, sc, tearDown := setupMockedTestServerWithConfigAndClientOptions(t, clientConfig, clientOpts)
+	t.Cleanup(tearDown)
+	defer sc.Close()
+
+	// Simulate that Commit is slow.
+	server.TestSpanner.PutExecutionTime(testutil.MethodCommitTransaction,
+		testutil.SimulatedExecutionTime{MinimumExecutionTime: time.Second})
+	ctx, cancel := context.WithTimeout(ctx, 10*time.Millisecond)
+	defer cancel()
+	_, err := sc.Apply(ctx, []*Mutation{})
+	if err == nil {
+		t.Fatal("missing deadline exceeded error")
+	}
+	if g, w := ErrCode(err), codes.DeadlineExceeded; g != w {
+		t.Fatalf("error code mismatch\n Got: %v\nWant: %v", g, w)
+	}
+	spannerError, ok := err.(*Error)
+	if !ok {
+		t.Fatal("not a Spanner error")
+	}
+	if spannerError.RequestID == "" {
+		t.Fatal("missing RequestID on error")
+	}
+}
+
+func TestRequestIDHeader_VerifyChannelNumber(t *testing.T) {
+	t.Parallel()
+
+	ctx := context.Background()
+	interceptorTracker := newInterceptorTracker()
+	clientOpts := []option.ClientOption{
+		option.WithGRPCDialOption(grpc.WithUnaryInterceptor(interceptorTracker.unaryClientInterceptor)),
+		option.WithGRPCDialOption(grpc.WithStreamInterceptor(interceptorTracker.streamClientInterceptor)),
+	}
+	clientConfig := ClientConfig{
+		SessionPoolConfig: SessionPoolConfig{
+			MinOpened: 100,
+			MaxOpened: 400,
+			incStep:   25,
+		},
+		NumChannels: 4,
+	}
+
+	_, sc, tearDown := setupMockedTestServerWithConfigAndClientOptions(t, clientConfig, clientOpts)
+	t.Cleanup(tearDown)
+	defer sc.Close()
+	// Wait for the session pool to be initialized.
+	sp := sc.idleSessions
+	waitFor(t, func() error {
+		sp.mu.Lock()
+		defer sp.mu.Unlock()
+		if uint64(sp.idleList.Len()) != clientConfig.MinOpened {
+			return fmt.Errorf("num open sessions mismatch\nWant: %d\nGot: %d", sp.MinOpened, sp.numOpened)
+		}
+		return nil
+	})
+	// Verify that we've seen request IDs for each channel number.
+	for channel := uint32(1); channel <= uint32(clientConfig.NumChannels); channel++ {
+		if !slices.ContainsFunc(interceptorTracker.unaryClientRequestIDSegments, func(segments *requestIDSegments) bool {
+			return segments.ChannelID == channel
+		}) {
+			t.Fatalf("missing channel %d in unary requests", channel)
+		}
+	}
+
+	// Execute MinOpened + 1 queries without closing the iterators.
+	// This will check out MinOpened + 1 sessions, which also triggers
+	// one more BatchCreateSessions call.
+	iterators := make([]*RowIterator, 0, clientConfig.MinOpened+1)
+	for i := 0; i < int(clientConfig.MinOpened)+1; i++ {
+		iter := sc.Single().Query(ctx, Statement{SQL: testutil.SelectFooFromBar})
+		iterators = append(iterators, iter)
+		_, err := iter.Next()
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+	// Verify that we've seen request IDs for each channel number.
+	for channel := uint32(1); channel <= uint32(clientConfig.NumChannels); channel++ {
+		if !slices.ContainsFunc(interceptorTracker.streamClientRequestIDSegments, func(segments *requestIDSegments) bool {
+			return segments.ChannelID == channel
+		}) {
+			t.Fatalf("missing channel %d in unary requests", channel)
+		}
+	}
+	// Verify that we've only seen channel numbers in the range [1, config.NumChannels].
+	for _, segmentsSlice := range [][]*requestIDSegments{interceptorTracker.streamClientRequestIDSegments, interceptorTracker.unaryClientRequestIDSegments} {
+		if slices.ContainsFunc(segmentsSlice, func(segments *requestIDSegments) bool {
+			return segments.ChannelID < 1 || segments.ChannelID > uint32(clientConfig.NumChannels)
+		}) {
+			t.Fatalf("invalid channel in requests: %v", segmentsSlice)
+		}
+	}
+
+	if g, w := interceptorTracker.unaryCallCount(), uint64(5); g != w {
+		t.Errorf("unaryClientCall is incorrect; got=%d want=%d", g, w)
+	}
+
+	if g, w := interceptorTracker.streamCallCount(), uint64(101); g != w {
+		t.Errorf("streamClientCall is incorrect; got=%d want=%d", g, w)
+	}
+
+	if err := interceptorTracker.validateRequestIDsMonotonicity(); err != nil {
+		t.Fatal(err)
+	}
+}
+
 func TestRequestIDInError(t *testing.T) {
 	cases := []struct {
 		name string