diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertProvider.cs index 42be248..fc14d5f 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.MySql/MySqlBulkInsertProvider.cs @@ -19,7 +19,7 @@ internal class MySqlBulkInsertProvider(ILogger logger) protected override string AddTableCopyBulkInsertId => $"ALTER TABLE {{0}} ADD {BulkInsertId} INT AUTO_INCREMENT PRIMARY KEY;"; /// - protected override string GetTempTableName(string tableName) => $"#_temp_bulk_insert_{tableName}"; + protected override string GetTempTableName(string tableName) => $"#_temp_bulk_insert_{tableName}_{Helpers.RandomString(6)}"; /// protected override MySqlBulkInsertOptions CreateDefaultOptions() => new() diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.Oracle/OracleBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.Oracle/OracleBulkInsertProvider.cs index 6f035ef..24333d6 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.Oracle/OracleBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.Oracle/OracleBulkInsertProvider.cs @@ -21,10 +21,10 @@ internal class OracleBulkInsertProvider(ILogger? logge /// /// - /// The temporary table name is generated with a GUID to ensure uniqueness, but limited to less than 30 characters, - /// because Oracle prior 12.2 has a limit of 30 characters for identifiers. + /// The temporary table name is generated with a random 8-character suffix to ensure uniqueness, and is limited to less than 30 characters, + /// because Oracle prior to 12.2 has a limit of 30 characters for identifiers. /// - protected override string GetTempTableName(string tableName) => $"#temp_bulk_insert_{Guid.NewGuid().ToString("N")[..8]}"; + protected override string GetTempTableName(string tableName) => $"#temp_bulk_insert_{Helpers.RandomString(8)}"; protected override OracleBulkInsertOptions CreateDefaultOptions() => new() { diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs index 1465530..bedff35 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs @@ -17,7 +17,7 @@ internal class SqlServerBulkInsertProvider(ILogger? protected override string AddTableCopyBulkInsertId => $"ALTER TABLE {{0}} ADD {BulkInsertId} INT IDENTITY PRIMARY KEY;"; /// - protected override string GetTempTableName(string tableName) => $"#_temp_bulk_insert_{tableName}"; + protected override string GetTempTableName(string tableName) => $"#_temp_bulk_insert_{tableName}_{Helpers.RandomString(6)}"; protected override SqlServerBulkInsertOptions CreateDefaultOptions() => new() { diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs index 547a72c..7edca6a 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert.Sqlite/SqliteBulkInsertProvider.cs @@ -24,7 +24,7 @@ internal class SqliteBulkInsertProvider(ILogger? logge protected override string AddTableCopyBulkInsertId => "--"; // No need to add an ID column in SQLite /// - protected override string GetTempTableName(string tableName) => $"_temp_bulk_insert_{Guid.NewGuid():N}"; + protected override string GetTempTableName(string tableName) => $"_temp_bulk_insert_{Helpers.RandomString(6)}"; /// protected override BulkInsertOptions CreateDefaultOptions() => new() diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderBase.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderBase.cs index c03e156..26d2610 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderBase.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderBase.cs @@ -19,7 +19,7 @@ internal abstract class BulkInsertProviderBase(ILogger? logg protected abstract string AddTableCopyBulkInsertId { get; } - protected virtual string GetTempTableName(string tableName) => $"_temp_bulk_insert_{tableName}"; + protected virtual string GetTempTableName(string tableName) => $"_temp_bulk_insert_{tableName}_{Helpers.RandomString(6)}"; protected override async IAsyncEnumerable BulkInsertReturnEntities( bool sync, @@ -124,6 +124,7 @@ protected override async Task BulkInsert( await connection.Close(sync, ctk); } } + private async Task PerformBulkInsertAsync( bool sync, DbContext context, diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Helpers.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Helpers.cs index f9ff635..e3e5429 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/Helpers.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Helpers.cs @@ -27,4 +27,21 @@ public static StringBuilder AppendColumns(this StringBuilder sb, IReadOnlyList c.QuotedColumName)); } + + /// + /// Generates a random alphanumeric string of the specified length. + /// + public static string RandomString(int length) + { + const string chars = "abcdefghijklmnopqrstuvwxyz0123456789"; + + var sb = new StringBuilder(length); + + for (var i = 0; i < length; i++) + { + sb.Append(chars[Random.Shared.Next(chars.Length)]); + } + + return sb.ToString(); + } } diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs index 6697dac..b09493c 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs @@ -235,6 +235,34 @@ public async Task InsertEntities_WithOpenTransaction_RollsBackOnFailure(InsertSt Assert.Empty(insertedEntities); } + [SkippableTheory] + [CombinatorialData] + public async Task InsertEntities_WithOpenTransaction_MultipleInserts(InsertStrategy strategy) + { + // Oracle: ORA-39822: A new direct path operation is not allowed in the current transaction. + Skip.If(_context.IsProvider(ProviderType.Oracle)); + + // Arrange + var entities = new List + { + new TestEntity { TestRun = _run, Name = $"{_run}_EntityWithTx1" }, + new TestEntity { TestRun = _run, Name = $"{_run}_EntityWithTx2" }, + new TestEntity { TestRun = _run, Name = $"{_run}_EntityWithTx3" }, + new TestEntity { TestRun = _run, Name = $"{_run}_EntityWithTx4" }, + }; + + // Act + await using var transaction = await _context.Database.BeginTransactionAsync(); + + var batches = entities.Chunk(2); + foreach (var batch in batches) + { + await _context.InsertWithStrategyAsync(strategy, batch.ToList()); + } + + await transaction.CommitAsync(); + } + [SkippableFact] public async Task ThrowsWhenUsingWrongConfigurationType() {