Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/dotnet-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,22 @@ jobs:

steps:
- uses: actions/checkout@v4

- name: Setup .NET 8.0
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x

- name: Setup .NET 9.0
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.x

- name: Restore dependencies
run: dotnet restore

- name: Build
run: dotnet build --no-restore

- name: Test
run: dotnet test --no-build --verbosity normal
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ internal class MySqlBulkInsertProvider(ILogger<MySqlBulkInsertProvider> logger)
protected override string GetTempTableName(string tableName) => $"#_temp_bulk_insert_{tableName}";

/// <inheritdoc />
protected override MySqlBulkInsertOptions CreateDefaultOptions() => new();
protected override MySqlBulkInsertOptions CreateDefaultOptions() => new()
{
Converters = [MySqlGeometryConverter.Instance]
};

/// <inheritdoc />
protected override IAsyncEnumerable<T> BulkInsertReturnEntities<T>(
Expand Down Expand Up @@ -71,11 +74,11 @@ CancellationToken ctk
if (sync)
{
// ReSharper disable once MethodHasAsyncOverloadWithCancellation
bulkCopy.WriteToServer(new EnumerableDataReader<T>(entities, properties));
bulkCopy.WriteToServer(new EnumerableDataReader<T>(entities, properties, options.Converters));
}
else
{
await bulkCopy.WriteToServerAsync(new EnumerableDataReader<T>(entities, properties), ctk);
await bulkCopy.WriteToServerAsync(new EnumerableDataReader<T>(entities, properties, options.Converters), ctk);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using MySqlConnector;

using NetTopologySuite.Geometries;

using PhenX.EntityFrameworkCore.BulkInsert.Abstractions;

namespace PhenX.EntityFrameworkCore.BulkInsert.MySql;

internal sealed class MySqlGeometryConverter : IBulkValueConverter
{
public static readonly MySqlGeometryConverter Instance = new();

private MySqlGeometryConverter()
{
}

public bool TryConvertValue(object source, out object result)
{
if (source is Geometry geometry)
{
result = MySqlGeometry.FromWkb(geometry.SRID, geometry.ToBinary());
return true;
}

result = source;
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="NetTopologySuite" Version="2.6.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Condition="'$(TargetFramework)' == 'net8.0'" Version="8.0.3" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Condition="'$(TargetFramework)' == 'net9.0'" Version="9.0.0-preview.3.efcore.9.0.0" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore.Metadata;

using NpgsqlTypes;

namespace PhenX.EntityFrameworkCore.BulkInsert.PostgreSql;

/// <summary>
/// Provides the type to write.
/// </summary>
public interface IPostgresTypeProvider
{
/// <summary>
/// Gets the type of a value before written to the output.
/// </summary>
/// <param name="property">The source property.</param>
/// <param name="result">The result type.</param>
/// <returns>Indicates if an object should be written.</returns>
bool TryGetType(IProperty property, out NpgsqlDbType result);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="NetTopologySuite" Version="2.6.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Condition="'$(TargetFramework)' == 'net8.0'" Version="8.0.11" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Condition="'$(TargetFramework)' == 'net9.0'" Version="9.0.4" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using PhenX.EntityFrameworkCore.BulkInsert.Options;

namespace PhenX.EntityFrameworkCore.BulkInsert.PostgreSql;

/// <summary>
/// Options specific to SQL Server bulk insert.
/// </summary>
public class PostgreSqlBulkInsertOptions : BulkInsertOptions
{
/// <summary>
/// A list of type providers.
/// </summary>
public List<IPostgresTypeProvider>? TypeProviders { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@
using NpgsqlTypes;

using PhenX.EntityFrameworkCore.BulkInsert.Metadata;
using PhenX.EntityFrameworkCore.BulkInsert.Options;

namespace PhenX.EntityFrameworkCore.BulkInsert.PostgreSql;

[UsedImplicitly]
internal class PostgreSqlBulkInsertProvider(ILogger<PostgreSqlBulkInsertProvider>? logger) : BulkInsertProviderBase<PostgreSqlDialectBuilder, BulkInsertOptions>(logger)
internal class PostgreSqlBulkInsertProvider(ILogger<PostgreSqlBulkInsertProvider>? logger) : BulkInsertProviderBase<PostgreSqlDialectBuilder, PostgreSqlBulkInsertOptions>(logger)
{
//language=sql
/// <inheritdoc />
Expand All @@ -32,9 +31,11 @@ private static string GetBinaryImportCommand(IReadOnlyList<ColumnMetadata> prope
}

/// <inheritdoc />
protected override BulkInsertOptions CreateDefaultOptions() => new()
protected override PostgreSqlBulkInsertOptions CreateDefaultOptions() => new()
{
BatchSize = 50_000,
Converters = [PostgreSqlGeometryConverter.Instance],
TypeProviders = [PostgreSqlGeometryConverter.Instance],
};

/// <inheritdoc />
Expand All @@ -45,7 +46,7 @@ protected override async Task BulkInsert<T>(
IEnumerable<T> entities,
string tableName,
IReadOnlyList<ColumnMetadata> columns,
BulkInsertOptions options,
PostgreSqlBulkInsertOptions options,
CancellationToken ctk)
{
var connection = (NpgsqlConnection)context.Database.GetDbConnection();
Expand All @@ -57,7 +58,7 @@ protected override async Task BulkInsert<T>(
: await connection.BeginBinaryImportAsync(command, ctk);

// The type mapping can be null for obvious types like string.
var columnTypes = columns.Select(GetPostgreSqlType).ToArray();
var columnTypes = columns.Select(c => GetPostgreSqlType(c, options)).ToArray();

foreach (var entity in entities)
{
Expand All @@ -74,7 +75,7 @@ protected override async Task BulkInsert<T>(
var columnIndex = 0;
foreach (var column in columns)
{
var value = column.GetValue(entity);
var value = column.GetValue(entity, options.Converters);

// Get the actual type, so that the writer can do the conversation to the target type automatically.
var type = columnTypes[columnIndex];
Expand Down Expand Up @@ -122,8 +123,20 @@ protected override async Task BulkInsert<T>(
}
}

private static NpgsqlDbType? GetPostgreSqlType(ColumnMetadata column)
private static NpgsqlDbType? GetPostgreSqlType(ColumnMetadata column, PostgreSqlBulkInsertOptions options)
{
var typeProviders = options.TypeProviders;
if (typeProviders is { Count: > 0 })
{
foreach (var typeProvider in typeProviders)
{
if (typeProvider.TryGetType(column.Property, out var type))
{
return type;
}
}
}

var mapping = column.Property.GetRelationalTypeMapping() as NpgsqlTypeMapping;

return mapping?.NpgsqlDbType;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore.Metadata;

using NetTopologySuite.Geometries;

using NpgsqlTypes;

using PhenX.EntityFrameworkCore.BulkInsert.Abstractions;

namespace PhenX.EntityFrameworkCore.BulkInsert.PostgreSql;

internal sealed class PostgreSqlGeometryConverter : IBulkValueConverter, IPostgresTypeProvider
{
public static readonly PostgreSqlGeometryConverter Instance = new();

private PostgreSqlGeometryConverter()
{
}

public bool TryConvertValue(object source, out object result)
{
if (source is Geometry geometry)
{
result = geometry.ToBinary();
return true;
}

result = source;
return false;
}

public bool TryGetType(IProperty property, out NpgsqlDbType result)
{
if (property.ClrType.IsAssignableTo(typeof(Geometry)))
{
result = NpgsqlDbType.Bytea;
return true;
}

result = default;
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.SqlServer.Types" Version="160.1000.6" />
<PackageReference Include="NetTopologySuite" Version="2.6.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Condition="'$(TargetFramework)' == 'net8.0'" Version="8.0.16" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Condition="'$(TargetFramework)' == 'net9.0'" Version="9.0.5" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal class SqlServerBulkInsertProvider(ILogger<SqlServerBulkInsertProvider>?
protected override SqlServerBulkInsertOptions CreateDefaultOptions() => new()
{
BatchSize = 50_000,
Converters = [SqlServerGeometryConverter.Instance]
};

/// <inheritdoc />
Expand Down Expand Up @@ -53,11 +54,11 @@ protected override async Task BulkInsert<T>(
if (sync)
{
// ReSharper disable once MethodHasAsyncOverloadWithCancellation
bulkCopy.WriteToServer(new EnumerableDataReader<T>(entities, columns));
bulkCopy.WriteToServer(new EnumerableDataReader<T>(entities, columns, options.Converters));
}
else
{
await bulkCopy.WriteToServerAsync(new EnumerableDataReader<T>(entities, columns), ctk);
await bulkCopy.WriteToServerAsync(new EnumerableDataReader<T>(entities, columns, options.Converters), ctk);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System.Data.SqlTypes;

using Microsoft.SqlServer.Types;

using NetTopologySuite.Geometries;

using PhenX.EntityFrameworkCore.BulkInsert.Abstractions;

namespace PhenX.EntityFrameworkCore.BulkInsert.SqlServer;

internal sealed class SqlServerGeometryConverter : IBulkValueConverter
{
public static readonly SqlServerGeometryConverter Instance = new();

private SqlServerGeometryConverter()
{
}

public bool TryConvertValue(object source, out object result)
{
if (source is Geometry geometry)
{
var reversed = Reverse(geometry);
result = SqlGeometry.STGeomFromWKB(new SqlBytes(reversed.AsBinary()), geometry.SRID);
return true;
}

result = source;
return false;
}

private static Geometry Reverse(Geometry input)
{
switch (input)
{
case Point point:
return Reverse(point);

case LineString lineString:
return Reverse(lineString);

case Polygon polygon:
return Reverse(polygon);

case MultiPoint multiPoint:
return Reverse(multiPoint);

case MultiLineString multiLineString:
return Reverse(multiLineString);

case MultiPolygon mpoly:
return Reverse(mpoly);

case GeometryCollection gc:
return Reverse(gc);

default:
throw new NotSupportedException($"Unsupported geometry type: {input.GeometryType}");
}
}

private static Point Reverse(Point input)
{
return input.Factory.CreatePoint(Swap(input.Coordinate));
}

private static LineString Reverse(LineString input)
{
return input.Factory.CreateLineString(Swap(input.Coordinates));
}

private static MultiPoint Reverse(MultiPoint input)
{
return input.Factory.CreateMultiPoint(input.Geometries.OfType<Point>().Select(Reverse).ToArray());
}

private static MultiLineString Reverse(MultiLineString input)
{
return input.Factory.CreateMultiLineString(input.Geometries.OfType<LineString>().Select(Reverse).ToArray());
}

private static MultiPolygon Reverse(MultiPolygon input)
{
return input.Factory.CreateMultiPolygon(input.Geometries.OfType<Polygon>().Select(Reverse).ToArray());
}

private static GeometryCollection Reverse(GeometryCollection input)
{
return input.Factory.CreateGeometryCollection(input.Geometries.Select(Reverse).ToArray());
}

private static Polygon Reverse(Polygon input)
{
var factory = input.Factory;

return input.Factory.CreatePolygon(
factory.CreateLinearRing(Swap(input.Shell.Coordinates)),
input.Holes.Select(h => factory.CreateLinearRing(Swap(h.Coordinates))).ToArray());
}

private static Coordinate Swap(Coordinate c) => new Coordinate(c.Y, c.X);

private static Coordinate[] Swap(Coordinate[] coords) => coords.Select(Swap).ToArray();

}
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ private static void FillValues<T>(T[] chunk, DbParameterCollection parameters, I
{
foreach (var column in columns)
{
var value = column.GetValue(entity);
var value = column.GetValue(entity, null);
parameters[p].Value = value;
p++;
}
Expand Down
Loading