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

     1  //go:build integration
     2  // +build integration
     3  
     4  package integration
     5  
     6  import (
     7  	"context"
     8  	"database/sql"
     9  	"errors"
    10  	"fmt"
    11  	"os"
    12  	"path"
    13  	"sync/atomic"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/stretchr/testify/require"
    18  	"google.golang.org/grpc/metadata"
    19  
    20  	"github.com/ydb-platform/ydb-go-sdk/v3"
    21  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xtest"
    22  	"github.com/ydb-platform/ydb-go-sdk/v3/meta"
    23  	"github.com/ydb-platform/ydb-go-sdk/v3/retry"
    24  	"github.com/ydb-platform/ydb-go-sdk/v3/sugar"
    25  	"github.com/ydb-platform/ydb-go-sdk/v3/trace"
    26  )
    27  
    28  func TestBasicExampleDatabaseSql(t *testing.T) {
    29  	folder := t.Name()
    30  
    31  	ctx, cancel := context.WithTimeout(xtest.Context(t), 42*time.Second)
    32  	defer cancel()
    33  
    34  	var totalConsumedUnits atomic.Uint64
    35  	defer func() {
    36  		t.Logf("total consumed units: %d", totalConsumedUnits.Load())
    37  	}()
    38  
    39  	ctx = meta.WithTrailerCallback(ctx, func(md metadata.MD) {
    40  		totalConsumedUnits.Add(meta.ConsumedUnits(md))
    41  	})
    42  
    43  	t.Run("sql.Open", func(t *testing.T) {
    44  		db, err := sql.Open("ydb", os.Getenv("YDB_CONNECTION_STRING"))
    45  		require.NoError(t, err)
    46  
    47  		err = db.PingContext(ctx)
    48  		require.NoError(t, err)
    49  
    50  		_, err = ydb.Unwrap(db)
    51  		require.NoError(t, err)
    52  
    53  		err = db.Close()
    54  		require.NoError(t, err)
    55  	})
    56  
    57  	t.Run("sql.OpenDB", func(t *testing.T) {
    58  		nativeDriver, err := ydb.Open(ctx, os.Getenv("YDB_CONNECTION_STRING"),
    59  			withMetrics(t, trace.DetailsAll, 0),
    60  			ydb.WithDiscoveryInterval(time.Second),
    61  		)
    62  		require.NoError(t, err)
    63  
    64  		defer func() {
    65  			// cleanup
    66  			_ = nativeDriver.Close(ctx)
    67  		}()
    68  
    69  		c, err := ydb.Connector(nativeDriver)
    70  		require.NoError(t, err)
    71  
    72  		defer func() {
    73  			// cleanup
    74  			_ = c.Close()
    75  		}()
    76  
    77  		db := sql.OpenDB(c)
    78  		defer func() {
    79  			// cleanup
    80  			_ = db.Close()
    81  		}()
    82  
    83  		err = db.PingContext(ctx)
    84  		require.NoError(t, err)
    85  
    86  		db.SetMaxOpenConns(50)
    87  		db.SetMaxIdleConns(50)
    88  
    89  		t.Run("prepare", func(t *testing.T) {
    90  			t.Run("scheme", func(t *testing.T) {
    91  				err = sugar.RemoveRecursive(ctx, nativeDriver, folder)
    92  				require.NoError(t, err)
    93  
    94  				err = sugar.MakeRecursive(ctx, nativeDriver, folder)
    95  				require.NoError(t, err)
    96  
    97  				t.Run("series", func(t *testing.T) {
    98  					var (
    99  						ctx       = ydb.WithQueryMode(ctx, ydb.SchemeQueryMode)
   100  						exists    bool
   101  						tablePath = path.Join(nativeDriver.Name(), folder, "series")
   102  					)
   103  
   104  					exists, err = sugar.IsTableExists(ctx, nativeDriver.Scheme(), tablePath)
   105  					require.NoError(t, err)
   106  
   107  					if exists {
   108  						_, err = db.ExecContext(ctx, `DROP TABLE `+"`"+tablePath+"`"+`;`)
   109  						require.NoError(t, err)
   110  					}
   111  
   112  					_, err = db.ExecContext(ctx, `
   113  						CREATE TABLE `+"`"+tablePath+"`"+` (
   114  							series_id Uint64,
   115  							title UTF8,
   116  							series_info UTF8,
   117  							release_date Date,
   118  							comment UTF8,
   119  							PRIMARY KEY (
   120  								series_id
   121  							)
   122  						);
   123  					`)
   124  					require.NoError(t, err)
   125  				})
   126  				t.Run("seasons", func(t *testing.T) {
   127  					var (
   128  						ctx       = ydb.WithQueryMode(ctx, ydb.SchemeQueryMode)
   129  						exists    bool
   130  						tablePath = path.Join(nativeDriver.Name(), folder, "seasons")
   131  					)
   132  
   133  					exists, err = sugar.IsTableExists(ctx, nativeDriver.Scheme(), tablePath)
   134  					require.NoError(t, err)
   135  
   136  					if exists {
   137  						_, err = db.ExecContext(ctx, `DROP TABLE `+"`"+tablePath+"`"+`;`)
   138  						require.NoError(t, err)
   139  					}
   140  
   141  					_, err = db.ExecContext(ctx, `
   142  						CREATE TABLE `+"`"+tablePath+"`"+` (
   143  							series_id Uint64,
   144  							season_id Uint64,
   145  							title UTF8,
   146  							first_aired Date,
   147  							last_aired Date,
   148  							PRIMARY KEY (
   149  								series_id,
   150  								season_id
   151  							)
   152  						);
   153  					`)
   154  					require.NoError(t, err)
   155  				})
   156  				t.Run("episodes", func(t *testing.T) {
   157  					var (
   158  						ctx       = ydb.WithQueryMode(ctx, ydb.SchemeQueryMode)
   159  						exists    bool
   160  						tablePath = path.Join(nativeDriver.Name(), folder, "episodes")
   161  					)
   162  
   163  					exists, err = sugar.IsTableExists(ctx, nativeDriver.Scheme(), tablePath)
   164  					require.NoError(t, err)
   165  
   166  					if exists {
   167  						_, err = db.ExecContext(ctx, `DROP TABLE `+"`"+tablePath+"`"+`;`)
   168  						require.NoError(t, err)
   169  					}
   170  
   171  					_, err = db.ExecContext(ctx, `
   172  						CREATE TABLE `+"`"+tablePath+"`"+` (
   173  							series_id Uint64,
   174  							season_id Uint64,
   175  							episode_id Uint64,
   176  							title UTF8,
   177  							air_date Date,
   178  							views Uint64,
   179  							PRIMARY KEY (
   180  								series_id,
   181  								season_id,
   182  								episode_id
   183  							)
   184  						);
   185  					`)
   186  					require.NoError(t, err)
   187  				})
   188  			})
   189  		})
   190  
   191  		t.Run("batch", func(t *testing.T) {
   192  			t.Run("upsert", func(t *testing.T) {
   193  				err = retry.Do(ctx, db, func(ctx context.Context, cc *sql.Conn) error {
   194  					stmt, err := cc.PrepareContext(ctx, `
   195  						PRAGMA TablePathPrefix("`+path.Join(nativeDriver.Name(), folder)+`");
   196  						
   197  						DECLARE $seriesData AS List<Struct<
   198  							series_id: Uint64,
   199  							title: Text,
   200  							series_info: Text,
   201  							release_date: Date,
   202  							comment: Optional<Text>>>;
   203  		
   204  						DECLARE $seasonsData AS List<Struct<
   205  							series_id: Uint64,
   206  							season_id: Uint64,
   207  							title: Text,
   208  							first_aired: Date,
   209  							last_aired: Date>>;
   210  		
   211  						DECLARE $episodesData AS List<Struct<
   212  							series_id: Uint64,
   213  							season_id: Uint64,
   214  							episode_id: Uint64,
   215  							title: Text,
   216  							air_date: Date>>;
   217  						
   218  						REPLACE INTO series SELECT * FROM AS_TABLE($seriesData);
   219  
   220  						REPLACE INTO seasons SELECT * FROM AS_TABLE($seasonsData);
   221  
   222  						REPLACE INTO episodes SELECT * FROM AS_TABLE($episodesData);
   223  					`)
   224  					if err != nil {
   225  						return fmt.Errorf("failed to prepare query: %w", err)
   226  					}
   227  					_, err = stmt.ExecContext(ctx,
   228  						sql.Named("seriesData", getSeriesData()),
   229  						sql.Named("seasonsData", getSeasonsData()),
   230  						sql.Named("episodesData", getEpisodesData()),
   231  					)
   232  					if err != nil {
   233  						return fmt.Errorf("failed to execute statement: %w", err)
   234  					}
   235  					return nil
   236  				}, retry.WithIdempotent(true))
   237  				require.NoError(t, err)
   238  			})
   239  		})
   240  
   241  		t.Run("query", func(t *testing.T) {
   242  			query := `
   243  				PRAGMA TablePathPrefix("` + path.Join(nativeDriver.Name(), folder) + `");
   244  
   245  				DECLARE $seriesID AS Uint64;
   246  				DECLARE $seasonID AS Uint64;
   247  				DECLARE $episodeID AS Uint64;
   248  
   249  				SELECT views 
   250  				FROM episodes 
   251  				WHERE 
   252  					series_id = $seriesID AND 
   253  					season_id = $seasonID AND 
   254  					episode_id = $episodeID;`
   255  			t.Run("explain", func(t *testing.T) {
   256  				row := db.QueryRowContext(
   257  					ydb.WithQueryMode(ctx, ydb.ExplainQueryMode), query,
   258  					sql.Named("seriesID", uint64(1)),
   259  					sql.Named("seasonID", uint64(1)),
   260  					sql.Named("episodeID", uint64(1)),
   261  				)
   262  				var (
   263  					ast  string
   264  					plan string
   265  				)
   266  
   267  				err = row.Scan(&ast, &plan)
   268  				require.NoError(t, err)
   269  
   270  				t.Logf("ast = %v", ast)
   271  				t.Logf("plan = %v", plan)
   272  			})
   273  			t.Run("increment", func(t *testing.T) {
   274  				t.Run("views", func(t *testing.T) {
   275  					err = retry.DoTx(ctx, db, func(ctx context.Context, tx *sql.Tx) (err error) {
   276  						var stmt *sql.Stmt
   277  						stmt, err = tx.PrepareContext(ctx, query)
   278  						if err != nil {
   279  							return fmt.Errorf("cannot prepare query: %w", err)
   280  						}
   281  
   282  						row := stmt.QueryRowContext(ctx,
   283  							sql.Named("seriesID", uint64(1)),
   284  							sql.Named("seasonID", uint64(1)),
   285  							sql.Named("episodeID", uint64(1)),
   286  						)
   287  						var views sql.NullFloat64
   288  						if err = row.Scan(&views); err != nil {
   289  							return fmt.Errorf("cannot scan views: %w", err)
   290  						}
   291  						if views.Valid {
   292  							return fmt.Errorf("unexpected valid views: %v", views.Float64)
   293  						}
   294  						// increment `views`
   295  						_, err = tx.ExecContext(ctx, `
   296  								PRAGMA TablePathPrefix("`+path.Join(nativeDriver.Name(), folder)+`");
   297  				
   298  								DECLARE $seriesID AS Uint64;
   299  								DECLARE $seasonID AS Uint64;
   300  								DECLARE $episodeID AS Uint64;
   301  								DECLARE $views AS Uint64;
   302  				
   303  								UPSERT INTO episodes ( series_id, season_id, episode_id, views )
   304  								VALUES ( $seriesID, $seasonID, $episodeID, $views );
   305  							`,
   306  							sql.Named("seriesID", uint64(1)),
   307  							sql.Named("seasonID", uint64(1)),
   308  							sql.Named("episodeID", uint64(1)),
   309  							sql.Named("views", uint64(views.Float64+1)), // increment views
   310  						)
   311  						if err != nil {
   312  							return fmt.Errorf("cannot upsert views: %w", err)
   313  						}
   314  						return nil
   315  					}, retry.WithIdempotent(true))
   316  					require.NoError(t, err)
   317  				})
   318  			})
   319  			t.Run("select", func(t *testing.T) {
   320  				t.Run("isolation", func(t *testing.T) {
   321  					t.Run("snapshot", func(t *testing.T) {
   322  						query := `
   323  							PRAGMA TablePathPrefix("` + path.Join(nativeDriver.Name(), folder) + `");
   324  				
   325  							DECLARE $seriesID AS Uint64;
   326  							DECLARE $seasonID AS Uint64;
   327  							DECLARE $episodeID AS Uint64;
   328  			
   329  							SELECT views FROM episodes 
   330  							WHERE 
   331  								series_id = $seriesID AND 
   332  								season_id = $seasonID AND 
   333  								episode_id = $episodeID;
   334  						`
   335  						err = retry.DoTx(ctx, db,
   336  							func(ctx context.Context, tx *sql.Tx) error {
   337  								row := tx.QueryRowContext(ctx, query,
   338  									sql.Named("seriesID", uint64(1)),
   339  									sql.Named("seasonID", uint64(1)),
   340  									sql.Named("episodeID", uint64(1)),
   341  								)
   342  								var views sql.NullFloat64
   343  								if err = row.Scan(&views); err != nil {
   344  									return fmt.Errorf("cannot select current views: %w", err)
   345  								}
   346  								if !views.Valid {
   347  									return fmt.Errorf("unexpected invalid views: %v", views)
   348  								}
   349  								if views.Float64 != 1 {
   350  									return fmt.Errorf("unexpected views value: %v", views)
   351  								}
   352  								return nil
   353  							},
   354  							retry.WithIdempotent(true),
   355  							retry.WithTxOptions(&sql.TxOptions{
   356  								Isolation: sql.LevelSnapshot,
   357  								ReadOnly:  true,
   358  							}),
   359  						)
   360  						if !errors.Is(err, context.DeadlineExceeded) {
   361  							require.NoError(t, err)
   362  						}
   363  					})
   364  				})
   365  				t.Run("scan", func(t *testing.T) {
   366  					t.Run("query", func(t *testing.T) {
   367  						var (
   368  							seriesID  *uint64
   369  							seasonID  *uint64
   370  							episodeID *uint64
   371  							title     *string
   372  							airDate   *time.Time
   373  							views     sql.NullFloat64
   374  							query     = `
   375  								PRAGMA TablePathPrefix("` + path.Join(nativeDriver.Name(), folder) + `");
   376  					
   377  								DECLARE $seriesID AS Optional<Uint64>;
   378  								DECLARE $seasonID AS Optional<Uint64>;
   379  								DECLARE $episodeID AS Optional<Uint64>;
   380  				
   381  								SELECT 
   382  									series_id,
   383  									season_id,
   384  									episode_id,
   385  									title,
   386  									air_date,
   387  									views
   388  								FROM episodes
   389  								WHERE 
   390  									(series_id >= $seriesID OR $seriesID IS NULL) AND
   391  									(season_id >= $seasonID OR $seasonID IS NULL) AND
   392  									(episode_id >= $episodeID OR $episodeID IS NULL) 
   393  								ORDER BY 
   394  									series_id, season_id, episode_id;
   395  							`
   396  						)
   397  						err := retry.DoTx(ctx, db,
   398  							func(ctx context.Context, tx *sql.Tx) error {
   399  								rows, err := tx.QueryContext(ctx, query,
   400  									sql.Named("seriesID", seriesID),
   401  									sql.Named("seasonID", seasonID),
   402  									sql.Named("episodeID", episodeID),
   403  								)
   404  								if err != nil {
   405  									return err
   406  								}
   407  								defer func() {
   408  									_ = rows.Close()
   409  								}()
   410  								for rows.NextResultSet() {
   411  									for rows.Next() {
   412  										if err = rows.Scan(&seriesID, &seasonID, &episodeID, &title, &airDate, &views); err != nil {
   413  											return fmt.Errorf("cannot select current views: %w", err)
   414  										}
   415  										t.Logf("[%d][%d][%d] - %s %q (%d views)",
   416  											*seriesID, *seasonID, *episodeID, airDate.Format("2006-01-02"),
   417  											*title, uint64(views.Float64),
   418  										)
   419  									}
   420  								}
   421  								return rows.Err()
   422  							},
   423  							retry.WithIdempotent(true),
   424  							retry.WithTxOptions(&sql.TxOptions{Isolation: sql.LevelSnapshot, ReadOnly: true}),
   425  						)
   426  						if !errors.Is(err, context.DeadlineExceeded) {
   427  							require.NoError(t, err)
   428  						}
   429  					})
   430  				})
   431  			})
   432  		})
   433  	})
   434  }