Skip to content

Commit ad0d19c

Browse files
MySql support.
1 parent 236bf15 commit ad0d19c

14 files changed

Lines changed: 304 additions & 48 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,6 @@ fabric.properties
8888

8989
# Nuget assets
9090
/nupkgs
91+
92+
# Visual Studio Files
93+
.vs

PhenX.EntityFrameworkCore.BulkInsert.sln

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.12.35707.178 d17.12
5+
MinimumVisualStudioVersion = 10.0.40219.1
36
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhenX.EntityFrameworkCore.BulkInsert", "src\PhenX.EntityFrameworkCore.BulkInsert\PhenX.EntityFrameworkCore.BulkInsert.csproj", "{56CA0AE2-6EAB-4394-9E06-132558551251}"
47
EndProject
58
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhenX.EntityFrameworkCore.BulkInsert.PostgreSql", "src\PhenX.EntityFrameworkCore.BulkInsert.PostgreSql\PhenX.EntityFrameworkCore.BulkInsert.PostgreSql.csproj", "{F37308A8-1C3C-44D2-9440-670DF76A8C31}"
@@ -30,6 +33,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "root", "root", "{45366E91-4
3033
README.md = README.md
3134
EndProjectSection
3235
EndProject
36+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhenX.EntityFrameworkCore.BulkInsert.MySql", "src\PhenX.EntityFrameworkCore.BulkInsert.MySql\PhenX.EntityFrameworkCore.BulkInsert.MySql.csproj", "{17649766-EA68-4333-8DA8-47B014A8B2CC}"
37+
EndProject
3338
Global
3439
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3540
Debug|Any CPU = Debug|Any CPU
@@ -60,6 +65,13 @@ Global
6065
{450E859C-411F-4D67-A0B4-4E02C3D30E14}.Debug|Any CPU.Build.0 = Debug|Any CPU
6166
{450E859C-411F-4D67-A0B4-4E02C3D30E14}.Release|Any CPU.ActiveCfg = Release|Any CPU
6267
{450E859C-411F-4D67-A0B4-4E02C3D30E14}.Release|Any CPU.Build.0 = Release|Any CPU
68+
{17649766-EA68-4333-8DA8-47B014A8B2CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
69+
{17649766-EA68-4333-8DA8-47B014A8B2CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
70+
{17649766-EA68-4333-8DA8-47B014A8B2CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
71+
{17649766-EA68-4333-8DA8-47B014A8B2CC}.Release|Any CPU.Build.0 = Release|Any CPU
72+
EndGlobalSection
73+
GlobalSection(SolutionProperties) = preSolution
74+
HideSolutionNode = FALSE
6375
EndGlobalSection
6476
GlobalSection(NestedProjects) = preSolution
6577
{56CA0AE2-6EAB-4394-9E06-132558551251} = {CBEBA2A8-79E0-412E-93C1-C88F4473D78B}
@@ -68,5 +80,6 @@ Global
6880
{EDCCED5F-D456-45E2-81A6-1077977F042B} = {F8A83782-311C-454D-8B97-B3FB86478BF4}
6981
{E4EB1C53-575C-45F8-924A-93DC42E8ACCA} = {F8A83782-311C-454D-8B97-B3FB86478BF4}
7082
{450E859C-411F-4D67-A0B4-4E02C3D30E14} = {CBEBA2A8-79E0-412E-93C1-C88F4473D78B}
83+
{17649766-EA68-4333-8DA8-47B014A8B2CC} = {CBEBA2A8-79E0-412E-93C1-C88F4473D78B}
7184
EndGlobalSection
7285
EndGlobal
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using Microsoft.EntityFrameworkCore.Storage;
3+
using Microsoft.Extensions.Logging;
4+
5+
using MySqlConnector;
6+
7+
using PhenX.EntityFrameworkCore.BulkInsert.Options;
8+
9+
namespace PhenX.EntityFrameworkCore.BulkInsert.MySql;
10+
11+
internal class MySqlBulkInsertProvider : BulkInsertProviderBase<MySqlServerDialectBuilder>
12+
{
13+
public MySqlBulkInsertProvider(ILogger<MySqlBulkInsertProvider>? logger = null) : base(logger)
14+
{
15+
}
16+
17+
//language=sql
18+
/// <inheritdoc />
19+
protected override string CreateTableCopySql => "CREATE TEMPORARY TABLE {0} SELECT * FROM {1} WHERE 1 = 0;";
20+
21+
//language=sql
22+
/// <inheritdoc />
23+
protected override string AddTableCopyBulkInsertId => $"ALTER TABLE {{0}} ADD {BulkInsertId} INT AUTO_INCREMENT PRIMARY KEY;";
24+
25+
/// <inheritdoc />
26+
protected override string GetTempTableName(string tableName) => $"#_temp_bulk_insert_{tableName}";
27+
28+
/// <inheritdoc />
29+
protected override async Task BulkInsert<T>(
30+
bool sync,
31+
DbContext context,
32+
IEnumerable<T> entities,
33+
string tableName,
34+
PropertyAccessor[] properties,
35+
BulkInsertOptions options,
36+
CancellationToken ctk
37+
)
38+
{
39+
var connection = (MySqlConnection)context.Database.GetDbConnection();
40+
var sqlTransaction = context.Database.CurrentTransaction!.GetDbTransaction() as MySqlTransaction;
41+
42+
var bulkCopy = new MySqlBulkCopy(connection, sqlTransaction);
43+
bulkCopy.DestinationTableName = tableName;
44+
bulkCopy.BulkCopyTimeout = 60;
45+
46+
var sourceOrdinal = 0;
47+
foreach (var prop in properties)
48+
{
49+
bulkCopy.ColumnMappings.Add(new MySqlBulkCopyColumnMapping(sourceOrdinal, prop.ColumnName));
50+
sourceOrdinal++;
51+
}
52+
53+
if (sync)
54+
{
55+
// ReSharper disable once MethodHasAsyncOverloadWithCancellation
56+
bulkCopy.WriteToServer(new EnumerableDataReader<T>(entities, properties));
57+
}
58+
else
59+
{
60+
await bulkCopy.WriteToServerAsync(new EnumerableDataReader<T>(entities, properties), ctk);
61+
}
62+
}
63+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using Microsoft.EntityFrameworkCore.Infrastructure;
3+
4+
namespace PhenX.EntityFrameworkCore.BulkInsert.MySql;
5+
6+
/// <summary>
7+
/// DbContext options extension for SQL Server.
8+
/// </summary>
9+
public static class MySqlDbContextOptionsExtensions
10+
{
11+
/// <summary>
12+
/// Configures the DbContext to use the MySql bulk insert provider.
13+
/// </summary>
14+
public static DbContextOptionsBuilder UseBulkInsertMySql(this DbContextOptionsBuilder optionsBuilder)
15+
{
16+
var extension = optionsBuilder.Options.FindExtension<BulkInsertOptionsExtension<MySqlBulkInsertProvider>>() ?? new BulkInsertOptionsExtension<MySqlBulkInsertProvider>();
17+
18+
((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension);
19+
20+
return optionsBuilder;
21+
}
22+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using PhenX.EntityFrameworkCore.BulkInsert.Dialect;
2+
3+
namespace PhenX.EntityFrameworkCore.BulkInsert.MySql;
4+
5+
internal class MySqlServerDialectBuilder : SqlDialectBuilder
6+
{
7+
protected override string OpenDelimiter => "`";
8+
9+
protected override string CloseDelimiter => "`";
10+
11+
protected override bool SupportsMoveRows => false;
12+
13+
public override bool SupportsReturning => false;
14+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<ItemGroup>
4+
<None Include="..\..\images\icon.png" Link="icon.png">
5+
<PackagePath>\</PackagePath>
6+
<Pack>true</Pack>
7+
</None>
8+
<None Include="..\..\README.md" Link="README.md">
9+
<PackagePath>\</PackagePath>
10+
<Pack>true</Pack>
11+
</None>
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<ProjectReference Include="..\PhenX.EntityFrameworkCore.BulkInsert\PhenX.EntityFrameworkCore.BulkInsert.csproj" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Condition="'$(TargetFramework)' == 'net8.0'" Version="8.0.3" />
20+
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Condition="'$(TargetFramework)' == 'net9.0'" Version="9.0.0-preview.3.efcore.9.0.0" />
21+
</ItemGroup>
22+
23+
</Project>

src/PhenX.EntityFrameworkCore.BulkInsert/BulkInsertProviderBase.cs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,34 @@ private async Task<List<TResult>> CopyFromTempTableWithoutKeysAsync<T, TResult>(
108108
{
109109
var (schemaName, tableName, _) = GetTableInfo(context, typeof(T));
110110
var quotedTableName = QuoteTableName(schemaName, tableName);
111-
112111
var movedProperties = context.GetProperties(typeof(T), false);
113112
var returnedProperties = returnData ? context.GetProperties(typeof(T)) : [];
114113

114+
if (returnData && !SqlDialect.SupportsReturning)
115+
{
116+
var moveQuery = SqlDialect.BuildMoveDataSql<T>(context, tempTableName, quotedTableName, movedProperties, [], options, onConflict);
117+
118+
// Just copy the values first.
119+
await ExecuteAsync(sync, context, moveQuery, cancellationToken);
120+
121+
// Then query them.
122+
var selectQuery = SqlDialect.BuildSelectSql<T>(context, tempTableName, returnedProperties);
123+
124+
return await QueryAsync(sync, context, selectQuery, cancellationToken);
125+
}
126+
115127
var query = SqlDialect.BuildMoveDataSql<T>(context, tempTableName, quotedTableName, movedProperties, returnedProperties, options, onConflict);
116128

117129
if (returnData)
130+
{
131+
return await QueryAsync(sync, context, query, cancellationToken);
132+
}
133+
134+
// If not returning data, just execute the command
135+
await ExecuteAsync(sync, context, query, cancellationToken);
136+
return [];
137+
138+
static async Task<List<TResult>> QueryAsync(bool sync, DbContext context, string query, CancellationToken cancellationToken)
118139
{
119140
// Use EF to execute the query and return the results
120141
IQueryable<TResult> queryable = context
@@ -128,10 +149,6 @@ private async Task<List<TResult>> CopyFromTempTableWithoutKeysAsync<T, TResult>(
128149

129150
return await queryable.ToListAsync(cancellationToken: cancellationToken);
130151
}
131-
132-
// If not returning data, just execute the command
133-
await ExecuteAsync(sync, context, query, cancellationToken);
134-
return [];
135152
}
136153

137154
public async Task<List<T>> BulkInsertReturnEntities<T>(

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Linq.Expressions;
1+
using System.Linq.Expressions;
22
using System.Text;
33

44
using Microsoft.EntityFrameworkCore;
@@ -15,6 +15,7 @@ internal abstract class SqlDialectBuilder
1515

1616
protected virtual string ConcatOperator => "||";
1717
protected virtual bool SupportsMoveRows => true;
18+
public virtual bool SupportsReturning => true;
1819

1920
/// <summary>
2021
/// Gets the name of the column for a property in a given entity type.
@@ -122,6 +123,33 @@ WHERE TRUE
122123
return q.ToString();
123124
}
124125

126+
/// <summary>
127+
/// Builds the SQL for selecting data from one table.
128+
/// </summary>
129+
/// <param name="context">The DbContext</param>
130+
/// <param name="source">Source table name</param>
131+
/// <param name="insertedProperties">Properties to be copied</param>
132+
/// <typeparam name="T">Entity type</typeparam>
133+
/// <returns>The SQL query</returns>
134+
public virtual string BuildSelectSql<T>(DbContext context, string source,
135+
IProperty[] insertedProperties)
136+
{
137+
var insertedColumns = insertedProperties.Select(p => Quote(p.GetColumnName()));
138+
var insertedColumnList = string.Join(", ", insertedColumns);
139+
140+
var q = new StringBuilder();
141+
142+
q.AppendLine($"""
143+
SELECT {insertedColumnList}
144+
FROM {source}
145+
WHERE TRUE
146+
""");
147+
148+
q.AppendLine(";");
149+
150+
return q.ToString();
151+
}
152+
125153
/// <summary>
126154
/// Get the name of the excluded column for the ON CONFLICT clause.
127155
/// </summary>

src/PhenX.EntityFrameworkCore.BulkInsert/EnumerableDataReader.cs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Data;
1+
using System.Data;
22

33
namespace PhenX.EntityFrameworkCore.BulkInsert;
44

@@ -26,12 +26,29 @@ public EnumerableDataReader(IEnumerable<T> rows, PropertyAccessor[] properties)
2626

2727
public virtual object GetValue(int i)
2828
{
29-
if (_enumerator.Current != null)
29+
var current = _enumerator.Current;
30+
if (current == null)
3031
{
31-
return _properties[i].GetValue(_enumerator.Current);
32+
return DBNull.Value;
3233
}
3334

34-
return DBNull.Value;
35+
return _properties[i].GetValue(current);
36+
}
37+
38+
public int GetValues(object[] values)
39+
{
40+
var current = _enumerator.Current;
41+
if (current == null)
42+
{
43+
return 0;
44+
}
45+
46+
for (var i = 0; i < _properties.Length; i++)
47+
{
48+
values[i] = _properties[i].GetValue(current);
49+
}
50+
51+
return _properties.Length;
3552
}
3653

3754
public bool Read() => _enumerator.MoveNext();
@@ -58,8 +75,6 @@ public void Dispose()
5875

5976
public bool NextResult() => throw new NotImplementedException();
6077

61-
public int GetValues(object[] values) => throw new NotImplementedException();
62-
6378
public bool IsDBNull(int i) => GetValue(i) is DBNull;
6479

6580
public object this[int i] => throw new NotImplementedException();

src/PhenX.EntityFrameworkCore.BulkInsert/PhenX.EntityFrameworkCore.BulkInsert.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
</ItemGroup>
77

88
<ItemGroup>
9+
<InternalsVisibleTo Include="PhenX.EntityFrameworkCore.BulkInsert.MySql"/>
910
<InternalsVisibleTo Include="PhenX.EntityFrameworkCore.BulkInsert.PostgreSql"/>
1011
<InternalsVisibleTo Include="PhenX.EntityFrameworkCore.BulkInsert.SqlServer"/>
1112
<InternalsVisibleTo Include="PhenX.EntityFrameworkCore.BulkInsert.Sqlite"/>

0 commit comments

Comments
 (0)