github.com/ydb-platform/ydb-go-sdk/v3@v3.57.0/tests/integration/basic_example_native_test.go (about)

     1  //go:build integration
     2  // +build integration
     3  
     4  package integration
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"math"
    10  	"net/http"
    11  	"os"
    12  	"path"
    13  	"runtime/debug"
    14  	"strings"
    15  	"sync"
    16  	"sync/atomic"
    17  	"testing"
    18  	"time"
    19  
    20  	"github.com/stretchr/testify/require"
    21  	"google.golang.org/grpc"
    22  	grpcCodes "google.golang.org/grpc/codes"
    23  	"google.golang.org/grpc/metadata"
    24  
    25  	"github.com/ydb-platform/ydb-go-sdk/v3"
    26  	"github.com/ydb-platform/ydb-go-sdk/v3/balancers"
    27  	"github.com/ydb-platform/ydb-go-sdk/v3/config"
    28  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xsync"
    29  	"github.com/ydb-platform/ydb-go-sdk/v3/log"
    30  	"github.com/ydb-platform/ydb-go-sdk/v3/meta"
    31  	"github.com/ydb-platform/ydb-go-sdk/v3/sugar"
    32  	"github.com/ydb-platform/ydb-go-sdk/v3/table"
    33  	"github.com/ydb-platform/ydb-go-sdk/v3/table/options"
    34  	"github.com/ydb-platform/ydb-go-sdk/v3/table/result"
    35  	"github.com/ydb-platform/ydb-go-sdk/v3/table/result/named"
    36  	"github.com/ydb-platform/ydb-go-sdk/v3/table/types"
    37  	"github.com/ydb-platform/ydb-go-sdk/v3/trace"
    38  )
    39  
    40  type stats struct {
    41  	xsync.Mutex
    42  
    43  	inFlightSessions map[string]struct{}
    44  	openSessions     map[string]struct{}
    45  	inPoolSessions   map[string]struct{}
    46  	limit            int
    47  }
    48  
    49  func (s *stats) print(t testing.TB) {
    50  	s.Lock()
    51  	defer s.Unlock()
    52  	t.Log("stats:")
    53  	t.Log(" - limit            :", s.limit)
    54  	t.Log(" - open             :", len(s.openSessions))
    55  	t.Log(" - in-pool          :", len(s.inPoolSessions))
    56  	t.Log(" - in-flight        :", len(s.inFlightSessions))
    57  }
    58  
    59  func (s *stats) check(t testing.TB) {
    60  	s.Lock()
    61  	defer s.Unlock()
    62  	if s.limit < 0 {
    63  		t.Fatalf("negative limit: %d", s.limit)
    64  	}
    65  	if len(s.inFlightSessions) > len(s.inPoolSessions) {
    66  		t.Fatalf("len(in_flight) > len(pool) (%d > %d)", len(s.inFlightSessions), len(s.inPoolSessions))
    67  	}
    68  	if len(s.inPoolSessions) > s.limit {
    69  		t.Fatalf("len(pool) > limit (%d > %d)", len(s.inPoolSessions), s.limit)
    70  	}
    71  }
    72  
    73  func (s *stats) max() int {
    74  	s.Lock()
    75  	defer s.Unlock()
    76  	return s.limit
    77  }
    78  
    79  func (s *stats) addToOpen(t testing.TB, id string) {
    80  	defer s.check(t)
    81  
    82  	s.Lock()
    83  	defer s.Unlock()
    84  
    85  	if _, ok := s.openSessions[id]; ok {
    86  		t.Fatalf("session '%s' add to open sessions twice", id)
    87  	}
    88  
    89  	s.openSessions[id] = struct{}{}
    90  }
    91  
    92  func (s *stats) removeFromOpen(t testing.TB, id string) {
    93  	defer s.check(t)
    94  
    95  	s.Lock()
    96  	defer s.Unlock()
    97  
    98  	if _, ok := s.openSessions[id]; !ok {
    99  		t.Fatalf("session '%s' already removed from open sessions", id)
   100  	}
   101  
   102  	delete(s.openSessions, id)
   103  }
   104  
   105  func (s *stats) addToPool(t testing.TB, id string) {
   106  	defer s.check(t)
   107  
   108  	s.Lock()
   109  	defer s.Unlock()
   110  
   111  	if _, ok := s.inPoolSessions[id]; ok {
   112  		t.Fatalf("session '%s' add to pool twice", id)
   113  	}
   114  
   115  	s.inPoolSessions[id] = struct{}{}
   116  }
   117  
   118  func (s *stats) removeFromPool(t testing.TB, id string) {
   119  	defer s.check(t)
   120  
   121  	s.Lock()
   122  	defer s.Unlock()
   123  
   124  	if _, ok := s.inPoolSessions[id]; !ok {
   125  		t.Fatalf("session '%s' already removed from pool", id)
   126  	}
   127  
   128  	delete(s.inPoolSessions, id)
   129  }
   130  
   131  func (s *stats) addToInFlight(t testing.TB, id string) {
   132  	defer s.check(t)
   133  
   134  	s.Lock()
   135  	defer s.Unlock()
   136  
   137  	if _, ok := s.inFlightSessions[id]; ok {
   138  		t.Fatalf("session '%s' add to in-flight twice", id)
   139  	}
   140  
   141  	s.inFlightSessions[id] = struct{}{}
   142  }
   143  
   144  func (s *stats) removeFromInFlight(t testing.TB, id string) {
   145  	defer s.check(t)
   146  
   147  	s.Lock()
   148  	defer s.Unlock()
   149  
   150  	if _, ok := s.inFlightSessions[id]; !ok {
   151  		return
   152  	}
   153  
   154  	delete(s.inFlightSessions, id)
   155  }
   156  
   157  func TestBasicExampleNative(t *testing.T) { //nolint:gocyclo
   158  	folder := t.Name()
   159  
   160  	ctx, cancel := context.WithTimeout(context.Background(), 42*time.Second)
   161  	defer cancel()
   162  
   163  	var totalConsumedUnits atomic.Uint64
   164  	defer func() {
   165  		t.Logf("total consumed units: %d", totalConsumedUnits.Load())
   166  	}()
   167  
   168  	ctx = meta.WithTrailerCallback(ctx, func(md metadata.MD) {
   169  		totalConsumedUnits.Add(meta.ConsumedUnits(md))
   170  	})
   171  
   172  	s := &stats{
   173  		limit:            math.MaxInt32,
   174  		openSessions:     make(map[string]struct{}),
   175  		inPoolSessions:   make(map[string]struct{}),
   176  		inFlightSessions: make(map[string]struct{}),
   177  	}
   178  	defer func() {
   179  		s.Lock()
   180  		defer s.Unlock()
   181  		if len(s.inFlightSessions) != 0 {
   182  			t.Errorf("'in-flight' not a zero after closing table client: %v", s.inFlightSessions)
   183  		}
   184  		if len(s.openSessions) != 0 {
   185  			t.Errorf("'openSessions' not a zero after closing table client: %v", s.openSessions)
   186  		}
   187  		if len(s.inPoolSessions) != 0 {
   188  			t.Errorf("'inPoolSessions' not a zero after closing table client: %v", s.inPoolSessions)
   189  		}
   190  	}()
   191  
   192  	var (
   193  		limit = 50
   194  
   195  		sessionsMtx sync.Mutex
   196  		sessions    = make(map[string]struct{}, limit)
   197  
   198  		shutdowned atomic.Bool
   199  
   200  		shutdownTrace = trace.Table{
   201  			OnPoolSessionAdd: func(info trace.TablePoolSessionAddInfo) {
   202  				sessionsMtx.Lock()
   203  				defer sessionsMtx.Unlock()
   204  				sessions[info.Session.ID()] = struct{}{}
   205  			},
   206  			OnPoolGet: func(
   207  				info trace.TablePoolGetStartInfo,
   208  			) func(
   209  				trace.TablePoolGetDoneInfo,
   210  			) {
   211  				return func(info trace.TablePoolGetDoneInfo) {
   212  					if info.Session == nil {
   213  						return
   214  					}
   215  					if shutdowned.Load() {
   216  						return
   217  					}
   218  					if info.Session.Status() != table.SessionClosing {
   219  						return
   220  					}
   221  					sessionsMtx.Lock()
   222  					defer sessionsMtx.Unlock()
   223  					if _, has := sessions[info.Session.ID()]; !has {
   224  						return
   225  					}
   226  					t.Fatalf("old session returned from pool after shutdown")
   227  				}
   228  			},
   229  		}
   230  	)
   231  
   232  	db, err := ydb.Open(ctx,
   233  		os.Getenv("YDB_CONNECTION_STRING"),
   234  		ydb.WithAccessTokenCredentials(os.Getenv("YDB_ACCESS_TOKEN_CREDENTIALS")),
   235  		ydb.WithUserAgent("table/e2e"),
   236  		withMetrics(t, trace.DetailsAll, time.Second),
   237  		ydb.With(
   238  			config.WithOperationTimeout(time.Second*5),
   239  			config.WithOperationCancelAfter(time.Second*5),
   240  			config.ExcludeGRPCCodesForPessimization(grpcCodes.DeadlineExceeded),
   241  			config.WithGrpcOptions(
   242  				grpc.WithUnaryInterceptor(func(
   243  					ctx context.Context,
   244  					method string,
   245  					req, reply interface{},
   246  					cc *grpc.ClientConn,
   247  					invoker grpc.UnaryInvoker,
   248  					opts ...grpc.CallOption,
   249  				) error {
   250  					return invoker(ctx, method, req, reply, cc, opts...)
   251  				}),
   252  				grpc.WithStreamInterceptor(func(
   253  					ctx context.Context,
   254  					desc *grpc.StreamDesc,
   255  					cc *grpc.ClientConn,
   256  					method string,
   257  					streamer grpc.Streamer,
   258  					opts ...grpc.CallOption,
   259  				) (grpc.ClientStream, error) {
   260  					return streamer(ctx, desc, cc, method, opts...)
   261  				}),
   262  			),
   263  		),
   264  		ydb.WithBalancer(balancers.RandomChoice()),
   265  		ydb.WithDialTimeout(5*time.Second),
   266  		ydb.WithSessionPoolSizeLimit(limit),
   267  		ydb.WithConnectionTTL(5*time.Second),
   268  		ydb.WithLogger(
   269  			newLoggerWithMinLevel(t, log.FromString(os.Getenv("YDB_LOG_SEVERITY_LEVEL"))),
   270  			trace.MatchDetails(`ydb\.(driver|table|discovery|retry|scheme).*`),
   271  		),
   272  		ydb.WithPanicCallback(func(e interface{}) {
   273  			t.Fatalf("panic recovered:%v:\n%s", e, debug.Stack())
   274  		}),
   275  		ydb.WithTraceTable(
   276  			*shutdownTrace.Compose(
   277  				&trace.Table{
   278  					OnInit: func(
   279  						info trace.TableInitStartInfo,
   280  					) func(
   281  						trace.TableInitDoneInfo,
   282  					) {
   283  						return func(info trace.TableInitDoneInfo) {
   284  							s.WithLock(func() {
   285  								s.limit = info.Limit
   286  							})
   287  						}
   288  					},
   289  					OnSessionNew: func(
   290  						info trace.TableSessionNewStartInfo,
   291  					) func(
   292  						trace.TableSessionNewDoneInfo,
   293  					) {
   294  						return func(info trace.TableSessionNewDoneInfo) {
   295  							if info.Error == nil {
   296  								s.addToOpen(t, info.Session.ID())
   297  							}
   298  						}
   299  					},
   300  					OnSessionDelete: func(
   301  						info trace.TableSessionDeleteStartInfo,
   302  					) func(
   303  						trace.TableSessionDeleteDoneInfo,
   304  					) {
   305  						s.removeFromOpen(t, info.Session.ID())
   306  						return nil
   307  					},
   308  					OnPoolSessionAdd: func(info trace.TablePoolSessionAddInfo) {
   309  						s.addToPool(t, info.Session.ID())
   310  					},
   311  					OnPoolSessionRemove: func(info trace.TablePoolSessionRemoveInfo) {
   312  						s.removeFromPool(t, info.Session.ID())
   313  					},
   314  					OnPoolGet: func(
   315  						info trace.TablePoolGetStartInfo,
   316  					) func(
   317  						trace.TablePoolGetDoneInfo,
   318  					) {
   319  						return func(info trace.TablePoolGetDoneInfo) {
   320  							if info.Error == nil {
   321  								s.addToInFlight(t, info.Session.ID())
   322  							}
   323  						}
   324  					},
   325  					OnPoolPut: func(
   326  						info trace.TablePoolPutStartInfo,
   327  					) func(
   328  						trace.TablePoolPutDoneInfo,
   329  					) {
   330  						s.removeFromInFlight(t, info.Session.ID())
   331  						return nil
   332  					},
   333  				},
   334  			),
   335  		),
   336  	)
   337  	if err != nil {
   338  		t.Fatal(err)
   339  	}
   340  
   341  	defer func() {
   342  		// cleanup
   343  		_ = db.Close(ctx)
   344  	}()
   345  
   346  	if err = db.Table().Do(ctx, func(ctx context.Context, _ table.Session) error {
   347  		// hack for wait pool initializing
   348  		return nil
   349  	}); err != nil {
   350  		t.Fatalf("pool not initialized: %+v", err)
   351  	} else if s.max() != limit {
   352  		t.Fatalf("pool size not applied: %+v", s)
   353  	}
   354  
   355  	// prepare scheme
   356  	err = sugar.RemoveRecursive(ctx, db, folder)
   357  	if err != nil {
   358  		t.Fatal(err)
   359  	}
   360  	err = sugar.MakeRecursive(ctx, db, folder)
   361  	if err != nil {
   362  		t.Fatal(err)
   363  	}
   364  
   365  	t.Run("prepare", func(t *testing.T) {
   366  		t.Run("scheme", func(t *testing.T) {
   367  			t.Run("series", func(t *testing.T) {
   368  				err := db.Table().Do(ctx,
   369  					func(ctx context.Context, session table.Session) (err error) {
   370  						if _, err = session.DescribeTable(ctx, path.Join(db.Name(), folder, "series")); err == nil {
   371  							_ = session.DropTable(ctx, path.Join(db.Name(), folder, "series"))
   372  						}
   373  						return session.CreateTable(ctx, path.Join(db.Name(), folder, "series"),
   374  							options.WithColumn("series_id", types.Optional(types.TypeUint64)),
   375  							options.WithColumn("title", types.Optional(types.TypeText)),
   376  							options.WithColumn("series_info", types.Optional(types.TypeText)),
   377  							options.WithColumn("release_date", types.Optional(types.TypeDate)),
   378  							options.WithColumn("comment", types.Optional(types.TypeText)),
   379  							options.WithPrimaryKeyColumn("series_id"),
   380  						)
   381  					},
   382  					table.WithIdempotent(),
   383  				)
   384  				require.NoError(t, err)
   385  			})
   386  			t.Run("seasons", func(t *testing.T) {
   387  				err := db.Table().Do(ctx,
   388  					func(ctx context.Context, session table.Session) (err error) {
   389  						if _, err = session.DescribeTable(ctx, path.Join(db.Name(), folder, "seasons")); err == nil {
   390  							_ = session.DropTable(ctx, path.Join(db.Name(), folder, "seasons"))
   391  						}
   392  						return session.CreateTable(ctx, path.Join(db.Name(), folder, "seasons"),
   393  							options.WithColumn("series_id", types.Optional(types.TypeUint64)),
   394  							options.WithColumn("season_id", types.Optional(types.TypeUint64)),
   395  							options.WithColumn("title", types.Optional(types.TypeText)),
   396  							options.WithColumn("first_aired", types.Optional(types.TypeDate)),
   397  							options.WithColumn("last_aired", types.Optional(types.TypeDate)),
   398  							options.WithPrimaryKeyColumn("series_id", "season_id"),
   399  						)
   400  					},
   401  					table.WithIdempotent(),
   402  				)
   403  				require.NoError(t, err)
   404  			})
   405  			t.Run("episodes", func(t *testing.T) {
   406  				err := db.Table().Do(ctx,
   407  					func(ctx context.Context, session table.Session) (err error) {
   408  						if _, err = session.DescribeTable(ctx, path.Join(db.Name(), folder, "episodes")); err == nil {
   409  							_ = session.DropTable(ctx, path.Join(db.Name(), folder, "episodes"))
   410  						}
   411  						return session.CreateTable(ctx, path.Join(db.Name(), folder, "episodes"),
   412  							options.WithColumn("series_id", types.Optional(types.TypeUint64)),
   413  							options.WithColumn("season_id", types.Optional(types.TypeUint64)),
   414  							options.WithColumn("episode_id", types.Optional(types.TypeUint64)),
   415  							options.WithColumn("title", types.Optional(types.TypeText)),
   416  							options.WithColumn("air_date", types.Optional(types.TypeDate)),
   417  							options.WithColumn("views", types.Optional(types.TypeUint64)),
   418  							options.WithPrimaryKeyColumn("series_id", "season_id", "episode_id"),
   419  						)
   420  					},
   421  					table.WithIdempotent(),
   422  				)
   423  				require.NoError(t, err)
   424  			})
   425  		})
   426  	})
   427  
   428  	t.Run("describe", func(t *testing.T) {
   429  		t.Run("table", func(t *testing.T) {
   430  			t.Run("series", func(t *testing.T) {
   431  				err := db.Table().Do(ctx,
   432  					func(ctx context.Context, session table.Session) (err error) {
   433  						_, err = session.DescribeTable(ctx, path.Join(db.Name(), folder, "series"))
   434  						if err != nil {
   435  							return
   436  						}
   437  						return err
   438  					},
   439  					table.WithIdempotent(),
   440  				)
   441  				require.NoError(t, err)
   442  			})
   443  			t.Run("seasons", func(t *testing.T) {
   444  				err := db.Table().Do(ctx,
   445  					func(ctx context.Context, session table.Session) (err error) {
   446  						_, err = session.DescribeTable(ctx, path.Join(db.Name(), folder, "seasons"))
   447  						if err != nil {
   448  							return
   449  						}
   450  						return err
   451  					},
   452  					table.WithIdempotent(),
   453  				)
   454  				require.NoError(t, err)
   455  			})
   456  			t.Run("episodes", func(t *testing.T) {
   457  				err := db.Table().Do(ctx,
   458  					func(ctx context.Context, session table.Session) (err error) {
   459  						_, err = session.DescribeTable(ctx, path.Join(db.Name(), folder, "episodes"))
   460  						if err != nil {
   461  							return
   462  						}
   463  						return err
   464  					},
   465  					table.WithIdempotent(),
   466  				)
   467  				require.NoError(t, err)
   468  			})
   469  		})
   470  	})
   471  
   472  	t.Run("upsert", func(t *testing.T) {
   473  		t.Run("data", func(t *testing.T) {
   474  			writeTx := table.TxControl(
   475  				table.BeginTx(
   476  					table.WithSerializableReadWrite(),
   477  				),
   478  				table.CommitTx(),
   479  			)
   480  			err := db.Table().Do(ctx,
   481  				func(ctx context.Context, session table.Session) (err error) {
   482  					stmt, err := session.Prepare(ctx, `
   483  						PRAGMA TablePathPrefix("`+path.Join(db.Name(), folder)+`");
   484  
   485  						DECLARE $seriesData AS List<Struct<
   486  							series_id: Uint64,
   487  							title: Text,
   488  							series_info: Text,
   489  							release_date: Date,
   490  							comment: Optional<Text>>>;
   491  		
   492  						DECLARE $seasonsData AS List<Struct<
   493  							series_id: Uint64,
   494  							season_id: Uint64,
   495  							title: Text,
   496  							first_aired: Date,
   497  							last_aired: Date>>;
   498  		
   499  						DECLARE $episodesData AS List<Struct<
   500  							series_id: Uint64,
   501  							season_id: Uint64,
   502  							episode_id: Uint64,
   503  							title: Text,
   504  							air_date: Date>>;
   505  		
   506  						REPLACE INTO series
   507  						SELECT
   508  							series_id,
   509  							title,
   510  							series_info,
   511  							release_date,
   512  							comment
   513  						FROM AS_TABLE($seriesData);
   514  		
   515  						REPLACE INTO seasons
   516  						SELECT
   517  							series_id,
   518  							season_id,
   519  							title,
   520  							first_aired,
   521  							last_aired
   522  						FROM AS_TABLE($seasonsData);
   523  		
   524  						REPLACE INTO episodes
   525  						SELECT
   526  							series_id,
   527  							season_id,
   528  							episode_id,
   529  							title,
   530  							air_date
   531  						FROM AS_TABLE($episodesData);`,
   532  					)
   533  					if err != nil {
   534  						return err
   535  					}
   536  
   537  					_, _, err = stmt.Execute(ctx, writeTx, table.NewQueryParameters(
   538  						table.ValueParam("$seriesData", getSeriesData()),
   539  						table.ValueParam("$seasonsData", getSeasonsData()),
   540  						table.ValueParam("$episodesData", getEpisodesData()),
   541  					))
   542  					return err
   543  				},
   544  				table.WithIdempotent(),
   545  			)
   546  			require.NoError(t, err)
   547  		})
   548  	})
   549  
   550  	t.Run("increment", func(t *testing.T) {
   551  		t.Run("views", func(t *testing.T) {
   552  			err := db.Table().DoTx(ctx,
   553  				func(ctx context.Context, tx table.TransactionActor) (err error) {
   554  					var (
   555  						res   result.Result
   556  						views uint64
   557  					)
   558  					// select current value of `views`
   559  					res, err = tx.Execute(ctx, `
   560  						PRAGMA TablePathPrefix("`+path.Join(db.Name(), folder)+`");
   561  
   562  						DECLARE $seriesID AS Uint64;
   563  						DECLARE $seasonID AS Uint64;
   564  						DECLARE $episodeID AS Uint64;
   565  
   566  						SELECT
   567  							views
   568  						FROM
   569  							episodes
   570  						WHERE
   571  							series_id = $seriesID AND 
   572  							season_id = $seasonID AND 
   573  							episode_id = $episodeID;`,
   574  						table.NewQueryParameters(
   575  							table.ValueParam("$seriesID", types.Uint64Value(1)),
   576  							table.ValueParam("$seasonID", types.Uint64Value(1)),
   577  							table.ValueParam("$episodeID", types.Uint64Value(1)),
   578  						),
   579  					)
   580  					if err != nil {
   581  						return err
   582  					}
   583  					if err = res.NextResultSetErr(ctx); err != nil {
   584  						return err
   585  					}
   586  					if !res.NextRow() {
   587  						return fmt.Errorf("nothing rows")
   588  					}
   589  					if err = res.ScanNamed(
   590  						named.OptionalWithDefault("views", &views),
   591  					); err != nil {
   592  						return err
   593  					}
   594  					if err = res.Err(); err != nil {
   595  						return err
   596  					}
   597  					if err = res.Close(); err != nil {
   598  						return err
   599  					}
   600  					// increment `views`
   601  					res, err = tx.Execute(ctx, `
   602  						PRAGMA TablePathPrefix("`+path.Join(db.Name(), folder)+`");
   603  
   604  						DECLARE $seriesID AS Uint64;
   605  						DECLARE $seasonID AS Uint64;
   606  						DECLARE $episodeID AS Uint64;
   607  						DECLARE $views AS Uint64;
   608  
   609  						UPSERT INTO episodes ( series_id, season_id, episode_id, views )
   610  						VALUES ( $seriesID, $seasonID, $episodeID, $views );`,
   611  						table.NewQueryParameters(
   612  							table.ValueParam("$seriesID", types.Uint64Value(1)),
   613  							table.ValueParam("$seasonID", types.Uint64Value(1)),
   614  							table.ValueParam("$episodeID", types.Uint64Value(1)),
   615  							table.ValueParam("$views", types.Uint64Value(views+1)), // increment views
   616  						),
   617  					)
   618  					if err != nil {
   619  						return err
   620  					}
   621  					if err = res.Err(); err != nil {
   622  						return err
   623  					}
   624  					return res.Close()
   625  				},
   626  				table.WithIdempotent(),
   627  			)
   628  			require.NoError(t, err)
   629  		})
   630  	})
   631  
   632  	t.Run("lookup", func(t *testing.T) {
   633  		t.Run("views", func(t *testing.T) {
   634  			err = db.Table().Do(ctx,
   635  				func(ctx context.Context, s table.Session) (err error) {
   636  					var (
   637  						res   result.Result
   638  						views uint64
   639  					)
   640  					// select current value of `views`
   641  					_, res, err = s.Execute(ctx,
   642  						table.TxControl(
   643  							table.BeginTx(
   644  								table.WithOnlineReadOnly(),
   645  							),
   646  							table.CommitTx(),
   647  						), `
   648  						PRAGMA TablePathPrefix("`+path.Join(db.Name(), folder)+`");
   649  
   650  						DECLARE $seriesID AS Uint64;
   651  						DECLARE $seasonID AS Uint64;
   652  						DECLARE $episodeID AS Uint64;
   653  
   654  						SELECT
   655  							views
   656  						FROM
   657  							episodes
   658  						WHERE
   659  							series_id = $seriesID AND 
   660  							season_id = $seasonID AND 
   661  							episode_id = $episodeID;`,
   662  						table.NewQueryParameters(
   663  							table.ValueParam("$seriesID", types.Uint64Value(1)),
   664  							table.ValueParam("$seasonID", types.Uint64Value(1)),
   665  							table.ValueParam("$episodeID", types.Uint64Value(1)),
   666  						),
   667  					)
   668  					if err != nil {
   669  						return err
   670  					}
   671  					if !res.NextResultSet(ctx, "views") {
   672  						return fmt.Errorf("nothing result sets")
   673  					}
   674  					if !res.NextRow() {
   675  						return fmt.Errorf("nothing result rows")
   676  					}
   677  					if err = res.ScanWithDefaults(&views); err != nil {
   678  						return err
   679  					}
   680  					if err = res.Err(); err != nil {
   681  						return err
   682  					}
   683  					if err = res.Close(); err != nil {
   684  						return err
   685  					}
   686  					if views != 1 {
   687  						return fmt.Errorf("unexpected views value: %d", views)
   688  					}
   689  					return nil
   690  				},
   691  				table.WithIdempotent(),
   692  			)
   693  			require.NoError(t, err)
   694  		})
   695  	})
   696  
   697  	t.Run("sessions", func(t *testing.T) {
   698  		t.Run("shutdown", func(t *testing.T) {
   699  			urls := os.Getenv("YDB_SESSIONS_SHUTDOWN_URLS")
   700  			if len(urls) > 0 {
   701  				for _, url := range strings.Split(urls, ",") {
   702  					//nolint:gosec
   703  					_, err = http.Get(url)
   704  					require.NoError(t, err)
   705  				}
   706  				shutdowned.Store(true)
   707  			}
   708  		})
   709  	})
   710  
   711  	t.Run("ExecuteDataQuery", func(t *testing.T) {
   712  		var (
   713  			query = `
   714  					PRAGMA TablePathPrefix("` + path.Join(db.Name(), folder) + `");
   715  
   716  					DECLARE $seriesID AS Uint64;
   717  
   718  					SELECT
   719  						series_id,
   720  						title,
   721  						release_date
   722  					FROM
   723  						series
   724  					WHERE
   725  						series_id = $seriesID;`
   726  			readTx = table.TxControl(
   727  				table.BeginTx(
   728  					table.WithOnlineReadOnly(),
   729  				),
   730  				table.CommitTx(),
   731  			)
   732  		)
   733  		err := db.Table().Do(ctx,
   734  			func(ctx context.Context, s table.Session) (err error) {
   735  				var (
   736  					res   result.Result
   737  					id    *uint64
   738  					title *string
   739  					date  *time.Time
   740  				)
   741  				_, res, err = s.Execute(ctx, readTx, query,
   742  					table.NewQueryParameters(
   743  						table.ValueParam("$seriesID", types.Uint64Value(1)),
   744  					),
   745  					options.WithCollectStatsModeBasic(),
   746  				)
   747  				if err != nil {
   748  					return err
   749  				}
   750  				defer func() {
   751  					_ = res.Close()
   752  				}()
   753  				t.Logf("> select_simple_transaction:\n")
   754  				for res.NextResultSet(ctx) {
   755  					for res.NextRow() {
   756  						err = res.ScanNamed(
   757  							named.Optional("series_id", &id),
   758  							named.Optional("title", &title),
   759  							named.Optional("release_date", &date),
   760  						)
   761  						if err != nil {
   762  							return err
   763  						}
   764  						t.Logf(
   765  							"  > %d %s %s\n",
   766  							*id, *title, *date,
   767  						)
   768  					}
   769  				}
   770  				return res.Err()
   771  			},
   772  			table.WithIdempotent(),
   773  		)
   774  		if err != nil && !ydb.IsTimeoutError(err) {
   775  			require.NoError(t, err)
   776  		}
   777  	})
   778  
   779  	t.Run("StreamExecuteScanQuery", func(t *testing.T) {
   780  		query := `
   781  			PRAGMA TablePathPrefix("` + path.Join(db.Name(), folder) + `");
   782  
   783  			DECLARE $series AS List<UInt64>;
   784  
   785  			SELECT series_id, season_id, title, first_aired
   786  			FROM seasons
   787  			WHERE series_id IN $series;`
   788  		err := db.Table().Do(ctx,
   789  			func(ctx context.Context, s table.Session) (err error) {
   790  				var (
   791  					res      result.StreamResult
   792  					seriesID uint64
   793  					seasonID uint64
   794  					title    string
   795  					date     time.Time
   796  				)
   797  				res, err = s.StreamExecuteScanQuery(ctx, query,
   798  					table.NewQueryParameters(
   799  						table.ValueParam("$series",
   800  							types.ListValue(
   801  								types.Uint64Value(1),
   802  								types.Uint64Value(10),
   803  							),
   804  						),
   805  					),
   806  				)
   807  				if err != nil {
   808  					return err
   809  				}
   810  				defer func() {
   811  					_ = res.Close()
   812  				}()
   813  				t.Logf("> scan_query_select:\n")
   814  				for res.NextResultSet(ctx) {
   815  					for res.NextRow() {
   816  						err = res.ScanWithDefaults(&seriesID, &seasonID, &title, &date)
   817  						if err != nil {
   818  							return err
   819  						}
   820  						t.Logf("  > SeriesId: %d, SeasonId: %d, Title: %s, Air date: %s\n", seriesID, seasonID, title, date)
   821  					}
   822  				}
   823  				return res.Err()
   824  			},
   825  			table.WithIdempotent(),
   826  		)
   827  		require.NoError(t, err)
   828  	})
   829  
   830  	t.Run("StreamReadTable", func(t *testing.T) {
   831  		err := db.Table().Do(ctx,
   832  			func(ctx context.Context, s table.Session) (err error) {
   833  				var (
   834  					res   result.StreamResult
   835  					id    *uint64
   836  					title *string
   837  					date  *time.Time
   838  				)
   839  				res, err = s.StreamReadTable(ctx, path.Join(db.Name(), folder, "series"),
   840  					options.ReadOrdered(),
   841  					options.ReadColumn("series_id"),
   842  					options.ReadColumn("title"),
   843  					options.ReadColumn("release_date"),
   844  				)
   845  				if err != nil {
   846  					return err
   847  				}
   848  				defer func() {
   849  					_ = res.Close()
   850  				}()
   851  				for res.NextResultSet(ctx, "series_id", "title", "release_date") {
   852  					for res.NextRow() {
   853  						err = res.Scan(&id, &title, &date)
   854  						if err != nil {
   855  							return err
   856  						}
   857  						// t.Logf("  > %d %s %s\n", *id, *title, date.String())
   858  					}
   859  				}
   860  				if err = res.Err(); err != nil {
   861  					return err
   862  				}
   863  
   864  				if stats := res.Stats(); stats != nil {
   865  					for i := 0; ; i++ {
   866  						phase, ok := stats.NextPhase()
   867  						if !ok {
   868  							break
   869  						}
   870  						for {
   871  							tbl, ok := phase.NextTableAccess()
   872  							if !ok {
   873  								break
   874  							}
   875  							t.Logf(
   876  								"#  accessed %s: read=(%drows, %dbytes)\n",
   877  								tbl.Name, tbl.Reads.Rows, tbl.Reads.Bytes,
   878  							)
   879  						}
   880  					}
   881  				}
   882  
   883  				return res.Err()
   884  			},
   885  			table.WithIdempotent(),
   886  		)
   887  		require.NoError(t, err)
   888  	})
   889  }