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 }