Skip to content

Commit c252c3f

Browse files
triforcelyMichal Wilski
andauthored
Fix: Identity insert handling for merge (InsertOrUpdate) statements in SQLite (#138)
* fix: do not use identity column when identity insert is off #131 * chore: fix flags in integration test cases * chore: remove fire triggers flag * chore: add integration tests for SQLite insert or update case --------- Co-authored-by: Michal Wilski <[email protected]>
1 parent cbde6fa commit c252c3f

File tree

6 files changed

+86
-13
lines changed

6 files changed

+86
-13
lines changed

EFCore.BulkExtensions.Tests/BulkInsertOrUpdate/BulkInsertOrUpdateTests.cs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
using EFCore.BulkExtensions.SqlAdapters;
2-
using Microsoft.Data.SqlClient;
32
using System;
43
using System.Collections.Generic;
54
using System.Linq;
6-
using Xunit;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Microsoft.EntityFrameworkCore;
78
using Xunit.Abstractions;
89

910

@@ -307,8 +308,45 @@ public void BulkInsertOrUpdate_ReloadList_IsWorking(DbServerType dbServerType)
307308
Assert.True(ensureList[1].Id != 0);
308309
}
309310

311+
/// <summary>
312+
/// Covers: https://github.com/videokojot/EFCore.BulkExtensions.MIT/issues/131
313+
/// </summary>
314+
[Theory]
315+
[InlineData(DbServerType.SQLite)]
316+
public async Task BulkInsertOrUpdate_InsertsOrUpdatesByPropertyIn(DbServerType dbType)
317+
{
318+
await using var db = _dbFixture.GetDb(dbType);
319+
320+
var bulkConfig = new BulkConfig
321+
{
322+
UpdateByProperties = new List<string> { nameof(UniqueItem.FirstName), nameof(UniqueItem.LastName) }
323+
};
324+
325+
var mockFirstName = Guid.NewGuid().ToString();
326+
var mockLastName = Guid.NewGuid().ToString();
327+
328+
const int testSetSize = 2;
329+
var testSet = Enumerable.Range(0, testSetSize)
330+
.Select(x => new UniqueItem()
331+
{
332+
FirstName = mockFirstName + x,
333+
LastName = mockLastName + x
334+
});
335+
336+
// duplicate enumeration is intentional - I want to create set of duplicates
337+
// to verify that the "insert or update" (aka merge) works correctly and
338+
// allows for database generated identity columns while doing so
339+
var duplicateSet = testSet.Concat(testSet).ToList();
340+
341+
await db.BulkInsertOrUpdateAsync(duplicateSet, bulkConfig, null, null, CancellationToken.None);
342+
343+
Assert.Equal(testSetSize, await db.UniqueItems.CountAsync());
344+
// before fix, it attempted to insert identity column, so it would insert "0" in this case, as it is default(int)
345+
Assert.True(await db.UniqueItems.AllAsync(x => x.Id > 0));
346+
}
347+
310348
public class DatabaseFixture : BulkDbTestsFixture<SimpleBulkTestsContext>
311349
{
312350
protected override string DbName => nameof(BulkInsertOrUpdateTests);
313351
}
314-
}
352+
}

EFCore.BulkExtensions.Tests/BulkInsertOrUpdate/SimpleBulkTestsContext.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ public class SimpleBulkTestsContext : DbContext
1111
{
1212
public DbSet<SimpleItem> SimpleItems { get; set; } = null!;
1313

14-
1514
public DbSet<Entity_CustomColumnNames> EntityCustomColumnNames { get; set; } = null!;
15+
16+
public DbSet<UniqueItem> UniqueItems { get; set; } = null!;
1617

1718
public SimpleBulkTestsContext(DbContextOptions options)
1819
: base(options)
@@ -21,6 +22,9 @@ public SimpleBulkTestsContext(DbContextOptions options)
2122

2223
protected override void OnModelCreating(ModelBuilder modelBuilder)
2324
{
25+
modelBuilder.Entity<UniqueItem>()
26+
.HasIndex(x => new { x.FirstName, x.LastName })
27+
.IsUnique();
2428
}
2529

2630
public List<SimpleItem> GetItemsOfBulk(Guid bulkId)
@@ -43,6 +47,17 @@ public class SimpleItem
4347
public string? StringProperty { get; set; }
4448
}
4549

50+
public class UniqueItem
51+
{
52+
[Key]
53+
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
54+
public int Id { get; set; }
55+
56+
public string FirstName { get; set; } = "";
57+
58+
public string LastName { get; set; } = "";
59+
}
60+
4661
public class Entity_CustomColumnNames
4762
{
4863
[Column("Id")]

EFCore.BulkExtensions.Tests/EFCoreBulkTest.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Diagnostics;
1111
using System.Linq;
1212
using System.Text.Json;
13+
using Microsoft.Data.SqlClient;
1314
using Xunit;
1415

1516
namespace EFCore.BulkExtensions.Tests;
@@ -533,7 +534,7 @@ private static void RunInsertOrUpdate(bool isBulk, DbServerType dbServer)
533534
}
534535
if (isBulk)
535536
{
536-
var bulkConfig = new BulkConfig() { SetOutputIdentity = true, CalculateStats = true };
537+
var bulkConfig = new BulkConfig { SetOutputIdentity = true, CalculateStats = true, SqlBulkCopyOptions = SqlBulkCopyOptions.KeepIdentity};
537538
context.BulkInsertOrUpdate(entities, bulkConfig, (a) => WriteProgress(a));
538539
if (dbServer == DbServerType.SQLServer)
539540
{

EFCore.BulkExtensions.Tests/EFCoreBulkTestAsync.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Diagnostics;
99
using System.Linq;
1010
using System.Threading.Tasks;
11+
using Microsoft.Data.SqlClient;
1112
using Xunit;
1213

1314
namespace EFCore.BulkExtensions.Tests;
@@ -221,7 +222,7 @@ private static async Task RunInsertOrUpdateAsync(bool isBulk, DbServerType dbSer
221222
}
222223
if (isBulk)
223224
{
224-
var bulkConfig = new BulkConfig() { SetOutputIdentity = true, CalculateStats = true };
225+
var bulkConfig = new BulkConfig() { SetOutputIdentity = true, CalculateStats = true, SqlBulkCopyOptions = SqlBulkCopyOptions.KeepIdentity};
225226
await context.BulkInsertOrUpdateAsync(entities, bulkConfig);
226227
if (dbServer == DbServerType.SQLServer)
227228
{

EFCore.BulkExtensions.Tests/SqlQueryBuilderSqliteTests.cs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using EFCore.BulkExtensions.SqlAdapters.SQLite;
5-
using Xunit;
5+
using Microsoft.Data.SqlClient;
66

77
namespace EFCore.BulkExtensions.Tests;
88

99
public class SqlQueryBuilderSqliteTests
1010
{
1111
[Fact]
12-
public void MergeTableInsertOrUpdateWithoutOnConflictUpdateWhereSqlTest()
12+
public void MergeTableInsertOrUpdateWithoutOnConflictWithIdentityUpdateWhereSqlTest()
1313
{
14-
TableInfo tableInfo = GetTestTableInfo();
14+
TableInfo tableInfo = GetTestTableInfo(bulkCopyOptions: SqlBulkCopyOptions.KeepIdentity);
1515
tableInfo.IdentityColumnName = "ItemId";
1616
string actual = SqlQueryBuilderSqlite.InsertIntoTable(tableInfo, OperationType.InsertOrUpdate);
1717

@@ -23,10 +23,25 @@ public void MergeTableInsertOrUpdateWithoutOnConflictUpdateWhereSqlTest()
2323
Assert.Equal(expected, actual);
2424
}
2525

26+
[Fact]
27+
public void MergeTableInsertOrUpdateWithoutOnConflictWithoutIdentityUpdateWhereSqlTest()
28+
{
29+
TableInfo tableInfo = GetTestTableInfo();
30+
tableInfo.IdentityColumnName = "ItemId";
31+
string actual = SqlQueryBuilderSqlite.InsertIntoTable(tableInfo, OperationType.InsertOrUpdate);
32+
33+
string expected = @"INSERT INTO [Item] ([Name]) " +
34+
@"VALUES (@Name) " +
35+
@"ON CONFLICT([ItemId]) DO UPDATE SET [Name] = @Name " +
36+
@"WHERE [ItemId] = @ItemId;";
37+
38+
Assert.Equal(expected, actual);
39+
}
40+
2641
[Fact]
2742
public void MergeTableInsertOrUpdateWithOnConflictUpdateWhereSqlTest()
2843
{
29-
TableInfo tableInfo = GetTestTableInfo((existing, inserted) => $"{inserted}.ItemTimestamp > {existing}.ItemTimestamp");
44+
TableInfo tableInfo = GetTestTableInfo((existing, inserted) => $"{inserted}.ItemTimestamp > {existing}.ItemTimestamp", SqlBulkCopyOptions.KeepIdentity);
3045
tableInfo.IdentityColumnName = "ItemId";
3146
string actual = SqlQueryBuilderSqlite.InsertIntoTable(tableInfo, OperationType.InsertOrUpdate);
3247

@@ -38,7 +53,9 @@ public void MergeTableInsertOrUpdateWithOnConflictUpdateWhereSqlTest()
3853
Assert.Equal(expected, actual);
3954
}
4055

41-
private TableInfo GetTestTableInfo(Func<string, string, string>? onConflictUpdateWhereSql = null)
56+
private TableInfo GetTestTableInfo(
57+
Func<string, string, string>? onConflictUpdateWhereSql = null
58+
, SqlBulkCopyOptions? bulkCopyOptions = null)
4259
{
4360
var tableInfo = new TableInfo()
4461
{
@@ -52,7 +69,8 @@ private TableInfo GetTestTableInfo(Func<string, string, string>? onConflictUpdat
5269
PrimaryKeysPropertyColumnNameDict = new Dictionary<string, string> { { nameof(Item.ItemId), nameof(Item.ItemId) } },
5370
BulkConfig = new BulkConfig()
5471
{
55-
OnConflictUpdateWhereSql = onConflictUpdateWhereSql
72+
OnConflictUpdateWhereSql = onConflictUpdateWhereSql,
73+
SqlBulkCopyOptions = bulkCopyOptions ?? SqlBulkCopyOptions.Default
5674
}
5775
};
5876
const string nameText = nameof(Item.Name);

EFCore.BulkExtensions/SqlAdapters/SQLite/SqlQueryBuilderSqlite.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public static string InsertIntoTable(TableInfo tableInfo, OperationType operatio
4343
List<string> propertiesList = tableInfo.PropertyColumnNamesDict.Keys.ToList();
4444

4545
bool keepIdentity = tableInfo.BulkConfig.SqlBulkCopyOptions.HasFlag(SqlBulkCopyOptions.KeepIdentity);
46-
if (operationType == OperationType.Insert && !keepIdentity && tableInfo.HasIdentity)
46+
if (!tableInfo.InsertToTempTable && !keepIdentity && tableInfo.HasIdentity)
4747
{
4848
var identityPropertyName = tableInfo.PropertyColumnNamesDict.SingleOrDefault(a => a.Value == tableInfo.IdentityColumnName).Key;
4949
columnsList = columnsList.Where(a => a != tableInfo.IdentityColumnName).ToList();

0 commit comments

Comments
 (0)