Skip to content

Commit 0366de2

Browse files
Fixes
1 parent e2e8f34 commit 0366de2

9 files changed

Lines changed: 102 additions & 24 deletions

File tree

src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlBulkInsertProvider.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,11 @@ protected override string CreateTableCopySql(string tempNameName, TableMetadata
2929
return $"CREATE TEMPORARY TABLE {tempNameName} AS TABLE {tableInfo.QuotedTableName} WITH NO DATA;";
3030
}
3131

32-
private static string GetBinaryImportCommand(TableMetadata tableInfo, string tableName)
32+
private static string GetBinaryImportCommand(IReadOnlyList<PropertyMetadata> properties, string tableName)
3333
{
34-
var columns = tableInfo.GetProperties(false).Select(X => X.QuotedColumName);
35-
3634
var sql = new StringBuilder();
3735
sql.Append($"COPY {tableName} (");
38-
sql.AppendColumns(tableInfo.GetProperties(false));
36+
sql.AppendColumns(properties);
3937
sql.Append(") FROM STDIN (FORMAT BINARY)");
4038
return sql.ToString();
4139
}
@@ -53,7 +51,7 @@ protected override async Task BulkInsert<T>(
5351
{
5452
var connection = (NpgsqlConnection)context.Database.GetDbConnection();
5553

56-
var importCommand = GetBinaryImportCommand(tableInfo, tableName);
54+
var importCommand = GetBinaryImportCommand(properties, tableName);
5755

5856
var writer = sync
5957
// ReSharper disable once MethodHasAsyncOverloadWithCancellation
Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
1-
using PhenX.EntityFrameworkCore.BulkInsert.Dialect;
1+
using System.Text;
2+
3+
using PhenX.EntityFrameworkCore.BulkInsert.Dialect;
4+
using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
5+
using PhenX.EntityFrameworkCore.BulkInsert.Options;
26

37
namespace PhenX.EntityFrameworkCore.BulkInsert.PostgreSql;
48

59
internal class PostgreSqlDialectBuilder : SqlDialectBuilder
610
{
711
protected override string OpenDelimiter => "\"";
812
protected override string CloseDelimiter => "\"";
13+
14+
protected override void AppendConflictMatch<T>(StringBuilder sql, TableMetadata target, OnConflictOptions<T> conflict)
15+
{
16+
if (conflict.Match != null)
17+
{
18+
base.AppendConflictMatch(sql, target, conflict);
19+
}
20+
else if (target.PrimaryKey.Count > 0)
21+
{
22+
sql.Append(' ');
23+
sql.AppendLine("(");
24+
sql.AppendColumns(target.PrimaryKey);
25+
sql.AppendLine(")");
26+
}
27+
else
28+
{
29+
throw new InvalidOperationException("Table has no primary key that can be used for conflict detection.");
30+
}
31+
}
932
}

src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerBulkInsertProvider.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,20 @@ public SqlServerBulkInsertProvider(ILogger<SqlServerBulkInsertProvider>? logger
2929
protected override string CreateTableCopySql(string templNameName, TableMetadata tableInfo, IReadOnlyList<PropertyMetadata> columns)
3030
{
3131
var sb = new StringBuilder();
32-
sb.Append("SELECT");
33-
sb.AppendJoin(", ", columns.Select(x => x.QuotedColumName));
34-
sb.Append($"INTO {templNameName} FROM {tableInfo.QuotedTableName} WHERE 1 = 0;");
32+
sb.Append($"CREATE TABLE {templNameName}");
33+
sb.AppendLine("(");
34+
35+
foreach (var column in columns)
36+
{
37+
sb.Append($" {column.QuotedColumName} {column.StoreDefinition}");
38+
if (column != columns[^1])
39+
{
40+
sb.Append(',');
41+
}
42+
sb.AppendLine();
43+
}
44+
45+
sb.AppendLine(")");
3546

3647
return sb.ToString();
3748
}

src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerDialectBuilder.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
using System.Text;
22

3-
using Microsoft.Extensions.Primitives;
4-
53
using PhenX.EntityFrameworkCore.BulkInsert.Dialect;
64
using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
75
using PhenX.EntityFrameworkCore.BulkInsert.Options;

src/PhenX.EntityFrameworkCore.BulkInsert/Dialect/SqlDialectBuilder.cs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,7 @@ public virtual string BuildMoveDataSql<T>(
5959

6060
if (onConflictTyped.Update != null)
6161
{
62-
if (onConflictTyped.Match != null)
63-
{
64-
q.Append(' ');
65-
AppendConflictMatch(q, GetColumns(target, onConflictTyped.Match));
66-
}
62+
AppendConflictMatch(q, target, onConflictTyped);
6763

6864
if (onConflictTyped.Update != null)
6965
{
@@ -93,7 +89,8 @@ public virtual string BuildMoveDataSql<T>(
9389

9490
q.AppendLine(";");
9591

96-
return q.ToString();
92+
var result = q.ToString();
93+
return result;
9794
}
9895

9996
protected virtual void AppendDoNothing(StringBuilder sql, IEnumerable<PropertyMetadata> insertedProperties)
@@ -107,11 +104,15 @@ protected virtual void AppendOnConflictUpdate(StringBuilder sql, IEnumerable<str
107104
sql.AppendJoin(", ", updates);
108105
}
109106

110-
protected virtual void AppendConflictMatch(StringBuilder sql, IEnumerable<string> columns)
107+
protected virtual void AppendConflictMatch<T>(StringBuilder sql, TableMetadata target, OnConflictOptions<T> conflict)
111108
{
112-
sql.AppendLine("(");
113-
sql.AppendJoin(", ", columns);
114-
sql.AppendLine(")");
109+
if (conflict.Match != null)
110+
{
111+
sql.Append(' ');
112+
sql.AppendLine("(");
113+
sql.AppendJoin(", ", GetColumns(target, conflict.Match));
114+
sql.AppendLine(")");
115+
}
115116
}
116117

117118
protected virtual void AppendOnConflictStatement(StringBuilder sql)

src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/PropertyMetadata.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ internal sealed class PropertyMetadata(IProperty property, SqlDialectBuilder di
1515

1616
public string QuotedColumName { get; } = dialect.Quote(property.GetColumnName());
1717

18+
public string StoreDefinition { get; } = GetStoreDefinition(property);
19+
1820
public Type ClrType { get; } = property.ClrType;
1921

2022
public Type? ProviderClrType { get; } = property.GetProviderClrType();
@@ -54,6 +56,15 @@ internal sealed class PropertyMetadata(IProperty property, SqlDialectBuilder di
5456
return result;
5557
}
5658

59+
private static string GetStoreDefinition(IProperty property)
60+
{
61+
var typeMapping = property.GetRelationalTypeMapping();
62+
63+
var nullability = property.IsNullable ? "NULL" : "NOT NULL";
64+
65+
return $"{typeMapping.StoreType} {nullability}";
66+
}
67+
5768
public override string ToString()
5869
{
5970
return $"Name: {Name}, Column: {ColumnName}";

tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContext.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
using Microsoft.EntityFrameworkCore;
1+
using Microsoft.EntityFrameworkCore;
22
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
33

44
namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext;
55

66
public class TestDbContext : TestDbContextBase
77
{
88
public DbSet<TestEntity> TestEntities { get; set; } = null!;
9+
public DbSet<TestEntityWithGuidId> TestEntitiesWithGuidIds { get; set; } = null!;
910
public DbSet<TestEntityWithConverters> TestEntitiesWithConverters { get; set; } = null!;
1011

1112
protected override void OnModelCreating(ModelBuilder modelBuilder)
@@ -16,5 +17,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
1617
builder.Property(e => e.CreatedAt)
1718
.HasConversion(new DateTimeToBinaryConverter());
1819
});
20+
21+
modelBuilder.Entity<TestEntityWithGuidId>(builder =>
22+
{
23+
builder.Property(e => e.Id)
24+
.ValueGeneratedNever();
25+
});
1926
}
2027
}

tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntity.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext;
1010
[Table("test_entity")]
1111
public class TestEntity
1212
{
13-
1413
public int Id { get; set; }
1514

1615
[Column("name")]

tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,40 @@ await _context.ExecuteBulkInsertAsync(entities,
147147
Assert.Contains(insertedEntities, e => e.NumericEnumValue == NumericEnum.Second);
148148
}
149149

150+
[SkippableFact]
151+
public async Task InsertsEntities_MultipleTimes_WithGuidId()
152+
{
153+
// Arrange
154+
var entities = new List<TestEntityWithGuidId>
155+
{
156+
new TestEntityWithGuidId { Id = Guid.NewGuid(), TestRun = _run, Name = $"{_run}_Entity1" },
157+
new TestEntityWithGuidId { Id = Guid.NewGuid(), TestRun = _run, Name = $"{_run}_Entity2" }
158+
};
159+
160+
// Act
161+
await _context.ExecuteBulkInsertAsync(entities);
162+
163+
foreach (var entity in entities)
164+
{
165+
entity.Name = $"Updated_{entity.Name}";
166+
}
167+
168+
await _context.ExecuteBulkInsertAsync(entities,
169+
onConflict: new OnConflictOptions<TestEntityWithGuidId>
170+
{
171+
Update = e => e,
172+
});
173+
174+
// Assert
175+
var insertedEntities = _context.TestEntitiesWithGuidIds.Where(x => x.TestRun == _run).ToList();
176+
Assert.Equal(2, insertedEntities.Count);
177+
Assert.Contains(insertedEntities, e => e.Name == $"Updated_{_run}_Entity1");
178+
Assert.Contains(insertedEntities, e => e.Name == $"Updated_{_run}_Entity2");
179+
}
180+
150181
[SkippableFact]
151182
public async Task InsertsEntities_MultipleTimes_With_Conflict_On_Id()
152183
{
153-
Skip.If(_context.Database.ProviderName!.Contains("Postgres", StringComparison.InvariantCultureIgnoreCase));
154184
Skip.If(_context.Database.ProviderName!.Contains("SqlServer", StringComparison.InvariantCultureIgnoreCase));
155185

156186
// Arrange

0 commit comments

Comments
 (0)