@@ -314,4 +314,187 @@ private List<Task> GenerateTasks(string connectionString) => Enumerable.Range(0,
314314
315315 protected override async Task OnDisposeAsync ( ) =>
316316 await YdbConnection . ClearPool ( new YdbConnection ( _connectionStringTls ) ) ;
317+
318+ [ Fact ]
319+ public async Task BulkUpsertImporter_HappyPath_Add_Flush ( )
320+ {
321+ var tableName = $ "BulkImporter_{ Guid . NewGuid ( ) : N} ";
322+
323+ var conn = new YdbConnection ( _connectionStringTls ) ;
324+ await conn . OpenAsync ( ) ;
325+ try
326+ {
327+ await using ( var createCmd = conn . CreateCommand ( ) )
328+ {
329+ createCmd . CommandText = $@ "
330+ CREATE TABLE { tableName } (
331+ Id Int32,
332+ Name Utf8,
333+ PRIMARY KEY (Id)
334+ )" ;
335+ await createCmd . ExecuteNonQueryAsync ( ) ;
336+ }
337+
338+ var columns = new [ ] { "Id" , "Name" } ;
339+
340+ var importer = conn . BeginBulkUpsertImport ( tableName , columns ) ;
341+
342+ await importer . AddRowAsync ( [ YdbValue . MakeInt32 ( 1 ) , YdbValue . MakeUtf8 ( "Alice" ) ] ) ;
343+ await importer . AddRowAsync ( [ YdbValue . MakeInt32 ( 2 ) , YdbValue . MakeUtf8 ( "Bob" ) ] ) ;
344+ await importer . FlushAsync ( ) ;
345+
346+ await using ( var checkCmd = conn . CreateCommand ( ) )
347+ {
348+ checkCmd . CommandText = $ "SELECT COUNT(*) FROM { tableName } ";
349+ var count = Convert . ToInt32 ( await checkCmd . ExecuteScalarAsync ( ) ) ;
350+ Assert . Equal ( 2 , count ) ;
351+ }
352+
353+ importer = conn . BeginBulkUpsertImport ( tableName , columns ) ;
354+ await importer . AddRowAsync ( [ YdbValue . MakeInt32 ( 3 ) , YdbValue . MakeUtf8 ( "Charlie" ) ] ) ;
355+ await importer . AddRowAsync ( [ YdbValue . MakeInt32 ( 4 ) , YdbValue . MakeUtf8 ( "Diana" ) ] ) ;
356+ await importer . FlushAsync ( ) ;
357+
358+ await using ( var checkCmd = conn . CreateCommand ( ) )
359+ {
360+ checkCmd . CommandText = $ "SELECT Name FROM { tableName } ORDER BY Id";
361+ var names = new List < string > ( ) ;
362+ await using var reader = await checkCmd . ExecuteReaderAsync ( ) ;
363+ while ( await reader . ReadAsync ( ) )
364+ names . Add ( reader . GetString ( 0 ) ) ;
365+ Assert . Contains ( "Alice" , names ) ;
366+ Assert . Contains ( "Bob" , names ) ;
367+ Assert . Contains ( "Charlie" , names ) ;
368+ Assert . Contains ( "Diana" , names ) ;
369+ }
370+ }
371+ finally
372+ {
373+ await using var dropCmd = conn . CreateCommand ( ) ;
374+ dropCmd . CommandText = $ "DROP TABLE { tableName } ";
375+ await dropCmd . ExecuteNonQueryAsync ( ) ;
376+ }
377+ }
378+
379+ [ Fact ]
380+ public async Task BulkUpsertImporter_ThrowsOnInvalidRowCount ( )
381+ {
382+ var tableName = $ "BulkImporter_{ Guid . NewGuid ( ) : N} ";
383+ var conn = new YdbConnection ( _connectionStringTls ) ;
384+ await conn . OpenAsync ( ) ;
385+ try
386+ {
387+ await using ( var createCmd = conn . CreateCommand ( ) )
388+ {
389+ createCmd . CommandText = $@ "
390+ CREATE TABLE { tableName } (
391+ Id Int32,
392+ Name Utf8,
393+ PRIMARY KEY (Id)
394+ )" ;
395+ await createCmd . ExecuteNonQueryAsync ( ) ;
396+ }
397+
398+ var columns = new [ ] { "Id" , "Name" } ;
399+
400+ var importer = conn . BeginBulkUpsertImport ( tableName , columns ) ;
401+
402+ var badRow = new object ? [ ] { YdbValue . MakeInt32 ( 1 ) } ;
403+ await Assert . ThrowsAsync < ArgumentException > ( async ( ) => await importer . AddRowAsync ( [ badRow ] ) ) ;
404+
405+ await Assert . ThrowsAsync < ArgumentException > ( async ( ) =>
406+ {
407+ await importer . AddRowAsync ( [
408+ new object ? [ ] { YdbValue . MakeInt32 ( 2 ) }
409+ ] ) ;
410+ } ) ;
411+ }
412+ finally
413+ {
414+ await using var dropCmd = conn . CreateCommand ( ) ;
415+ dropCmd . CommandText = $ "DROP TABLE { tableName } ";
416+ await dropCmd . ExecuteNonQueryAsync ( ) ;
417+ }
418+ }
419+
420+ [ Fact ]
421+ public async Task BulkUpsertImporter_MultipleImporters_Parallel ( )
422+ {
423+ var table1 = $ "BulkImporter_{ Guid . NewGuid ( ) : N} _1";
424+ var table2 = $ "BulkImporter_{ Guid . NewGuid ( ) : N} _2";
425+
426+ var conn = new YdbConnection ( _connectionStringTls ) ;
427+ await conn . OpenAsync ( ) ;
428+ try
429+ {
430+ foreach ( var table in new [ ] { table1 , table2 } )
431+ {
432+ await using var createCmd = conn . CreateCommand ( ) ;
433+ createCmd . CommandText = $@ "CREATE TABLE { table } (
434+ Id Int32,
435+ Name Utf8,
436+ PRIMARY KEY (Id)
437+ )" ;
438+ await createCmd . ExecuteNonQueryAsync ( ) ;
439+ }
440+
441+ var columns = new [ ] { "Id" , "Name" } ;
442+
443+ await Task . WhenAll (
444+ Task . Run ( async ( ) =>
445+ {
446+ var importer = conn . BeginBulkUpsertImport ( table1 , columns ) ;
447+ var rows = Enumerable . Range ( 0 , 20 )
448+ . Select ( i => new object ? [ ] { YdbValue . MakeInt32 ( i ) , YdbValue . MakeUtf8 ( $ "A{ i } ") } )
449+ . ToArray ( ) ;
450+ foreach ( var row in rows )
451+ await importer . AddRowAsync ( row ) ;
452+ await importer . FlushAsync ( ) ;
453+ } ) ,
454+ Task . Run ( async ( ) =>
455+ {
456+ var importer = conn . BeginBulkUpsertImport ( table2 , columns ) ;
457+ var rows = Enumerable . Range ( 0 , 20 )
458+ . Select ( i => new object ? [ ] { YdbValue . MakeInt32 ( i ) , YdbValue . MakeUtf8 ( $ "B{ i } ") } )
459+ . ToArray ( ) ;
460+ foreach ( var row in rows )
461+ await importer . AddRowAsync ( row ) ;
462+ await importer . FlushAsync ( ) ;
463+ } )
464+ ) ;
465+
466+ foreach ( var table in new [ ] { table1 , table2 } )
467+ {
468+ await using var checkCmd = conn . CreateCommand ( ) ;
469+ checkCmd . CommandText = $ "SELECT COUNT(*) FROM { table } ";
470+ var count = Convert . ToInt32 ( await checkCmd . ExecuteScalarAsync ( ) ) ;
471+ Assert . Equal ( 20 , count ) ;
472+ }
473+ }
474+ finally
475+ {
476+ foreach ( var table in new [ ] { table1 , table2 } )
477+ {
478+ await using var dropCmd = conn . CreateCommand ( ) ;
479+ dropCmd . CommandText = $ "DROP TABLE { table } ";
480+ await dropCmd . ExecuteNonQueryAsync ( ) ;
481+ }
482+ }
483+ }
484+
485+ [ Fact ]
486+ public async Task BulkUpsertImporter_ThrowsOnNonexistentTable ( )
487+ {
488+ var tableName = $ "Nonexistent_{ Guid . NewGuid ( ) : N} ";
489+ var conn = new YdbConnection ( _connectionStringTls ) ;
490+ await conn . OpenAsync ( ) ;
491+
492+ var columns = new [ ] { "Id" , "Name" } ;
493+
494+ var importer = conn . BeginBulkUpsertImport ( tableName , columns ) ;
495+
496+ await importer . AddRowAsync ( [ YdbValue . MakeInt32 ( 1 ) , YdbValue . MakeUtf8 ( "NotExists" ) ] ) ;
497+
498+ await Assert . ThrowsAsync < YdbException > ( async ( ) => { await importer . FlushAsync ( ) ; } ) ;
499+ }
317500}
0 commit comments