Skip to content

Commit

Permalink
Added support for spatial projections. (#2567)
Browse files Browse the repository at this point in the history
  • Loading branch information
PascalSenn committed Nov 21, 2020
1 parent 7c25702 commit 9905252
Show file tree
Hide file tree
Showing 17 changed files with 508 additions and 7 deletions.
Expand Up @@ -3,19 +3,19 @@

namespace HotChocolate.Data.Projections
{
public abstract class ProjectionProviderExtensions
: ConventionExtension<ProjectionProviderDefinition>,
IProjectionProviderExtension,
IProjectionProviderConvention
public class ProjectionProviderExtension
: ConventionExtension<ProjectionProviderDefinition>
, IProjectionProviderExtension
, IProjectionProviderConvention
{
private Action<IProjectionProviderDescriptor>? _configure;

protected ProjectionProviderExtensions()
protected ProjectionProviderExtension()
{
_configure = Configure;
}

public ProjectionProviderExtensions(Action<IProjectionProviderDescriptor> configure)
public ProjectionProviderExtension(Action<IProjectionProviderDescriptor> configure)
{
_configure = configure ??
throw new ArgumentNullException(nameof(configure));
Expand Down
Expand Up @@ -110,7 +110,7 @@ public void Merge_Should_Merge_ProviderExtensions()
x => Assert.Equal(provider2, x));
}

private class MockProviderExtensions : ProjectionProviderExtensions
private class MockProviderExtensions : ProjectionProviderExtension
{
}

Expand Down
15 changes: 15 additions & 0 deletions src/HotChocolate/Spatial/HotChocolate.Spatial.sln
Expand Up @@ -57,6 +57,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Data.Spatial",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Data", "..\Data\src\Data\HotChocolate.Data.csproj", "{2AB20A1C-C927-4596-A003-07E2331C2943}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HotChocolate.Data.Projections.Spatial.SqlServer.Tests", "test\Data.Projections.SqlServer.Tests\HotChocolate.Data.Projections.Spatial.SqlServer.Tests.csproj", "{C30D7D2B-6D7E-4F2E-A237-A7C6B2626838}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -91,6 +93,7 @@ Global
{553B323C-6436-4F21-9794-637203E1E371} = {4EE990B2-C327-46DA-8FE8-F95AC228E47F}
{5E628010-C2DF-4000-9C9F-9A6A60EC62B7} = {91887A91-7B1C-4287-A1E0-BD4E0DAF24C7}
{2AB20A1C-C927-4596-A003-07E2331C2943} = {882EC02D-5E1D-41F5-AD9F-AA06E31D133A}
{C30D7D2B-6D7E-4F2E-A237-A7C6B2626838} = {4EE990B2-C327-46DA-8FE8-F95AC228E47F}
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D68A0AB9-871A-487B-8D12-1A7544D81B9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
Expand Down Expand Up @@ -345,5 +348,17 @@ Global
{2AB20A1C-C927-4596-A003-07E2331C2943}.Release|x64.Build.0 = Release|Any CPU
{2AB20A1C-C927-4596-A003-07E2331C2943}.Release|x86.ActiveCfg = Release|Any CPU
{2AB20A1C-C927-4596-A003-07E2331C2943}.Release|x86.Build.0 = Release|Any CPU
{C30D7D2B-6D7E-4F2E-A237-A7C6B2626838}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C30D7D2B-6D7E-4F2E-A237-A7C6B2626838}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C30D7D2B-6D7E-4F2E-A237-A7C6B2626838}.Debug|x64.ActiveCfg = Debug|Any CPU
{C30D7D2B-6D7E-4F2E-A237-A7C6B2626838}.Debug|x64.Build.0 = Debug|Any CPU
{C30D7D2B-6D7E-4F2E-A237-A7C6B2626838}.Debug|x86.ActiveCfg = Debug|Any CPU
{C30D7D2B-6D7E-4F2E-A237-A7C6B2626838}.Debug|x86.Build.0 = Debug|Any CPU
{C30D7D2B-6D7E-4F2E-A237-A7C6B2626838}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C30D7D2B-6D7E-4F2E-A237-A7C6B2626838}.Release|Any CPU.Build.0 = Release|Any CPU
{C30D7D2B-6D7E-4F2E-A237-A7C6B2626838}.Release|x64.ActiveCfg = Release|Any CPU
{C30D7D2B-6D7E-4F2E-A237-A7C6B2626838}.Release|x64.Build.0 = Release|Any CPU
{C30D7D2B-6D7E-4F2E-A237-A7C6B2626838}.Release|x86.ActiveCfg = Release|Any CPU
{C30D7D2B-6D7E-4F2E-A237-A7C6B2626838}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
@@ -0,0 +1,15 @@
using HotChocolate.Data.Projections.Expressions.Handlers;
using HotChocolate.Execution.Processing;
using HotChocolate.Utilities;
using NetTopologySuite.Geometries;

namespace HotChocolate.Data.Projections.Spatial
{
public class QueryableSpatialProjectionScalarHandler
: QueryableProjectionScalarHandler
{
public override bool CanHandle(ISelection selection) =>
selection.Field.Member is not null &&
typeof(Geometry).IsAssignableFrom(selection.Field.Member.GetReturnType());
}
}
@@ -0,0 +1,9 @@
namespace HotChocolate.Data.Projections.Spatial
{
public static class SpatialProjectionProviderDescriptorQueryableExtensions
{
public static IProjectionProviderDescriptor AddSpatialHandlers(
this IProjectionProviderDescriptor descriptor) =>
descriptor.RegisterFieldHandler<QueryableSpatialProjectionScalarHandler>();
}
}
@@ -0,0 +1,22 @@
using System;
using HotChocolate.Data.Filters;
using HotChocolate.Data.Filters.Spatial;
using HotChocolate.Execution.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace HotChocolate
{
public static class SpatialProjectionsRequestExecutorBuilderExtensions
{
public static IRequestExecutorBuilder AddSpatialProjections(
this IRequestExecutorBuilder builder)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}

return builder.ConfigureSchema(x => x.AddSpatialProjections());
}
}
}
@@ -0,0 +1,22 @@
using System;
using HotChocolate.Data.Projections.Spatial;
using HotChocolate.Data.Projections;

namespace HotChocolate
{
public static class SpatialProjectionsSchemaBuilderExtensions
{
public static ISchemaBuilder AddSpatialProjections(this ISchemaBuilder builder)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}

return builder.AddConvention<IProjectionConvention>(
new ProjectionConventionExtension(
x => x.AddProviderExtension(
new ProjectionProviderExtension(y => y.AddSpatialHandlers()))));
}
}
}
@@ -0,0 +1,38 @@
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Squadron;

namespace HotChocolate.Data.Projections.Spatial
{
public class DatabaseContext<T> : DbContext where T : class
{
private readonly PostgreSqlResource<PostgisConfig> _resource;
private readonly string _databaseName;
private bool _disposed;

public DatabaseContext(PostgreSqlResource<PostgisConfig> resource, string databaseName)
{
_resource = resource;
_databaseName = databaseName;
}

public DbSet<T> Data { get; set; } = default!;

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql(
_resource.GetConnectionString(_databaseName),
o => o.UseNetTopologySuite());
}

public override async ValueTask DisposeAsync()
{
await base.DisposeAsync();

if (!_disposed)
{
_disposed = true;
}
}
}
}
@@ -0,0 +1,25 @@
using HotChocolate.Execution;
using HotChocolate.Tests;
using Snapshooter;
using Snapshooter.Xunit;

namespace HotChocolate.Data.Projections.Spatial
{
public static class TestExtensions
{
public static void MatchSqlSnapshot(
this IExecutionResult? result,
string snapshotName)
{
if (result is { })
{
result.MatchSnapshot(snapshotName);
if (result.ContextData is { } &&
result.ContextData.TryGetValue("sql", out object? queryResult))
{
queryResult.MatchSnapshot(new SnapshotNameExtension(snapshotName + "_sql"));
}
}
}
}
}
@@ -0,0 +1,51 @@
using System.Linq;
using System.Reflection;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Query;
using System;
using Microsoft.EntityFrameworkCore.Storage;

namespace HotChocolate.Data.Projections.Spatial
{
public static class ToQueryStringExtensions
{
public static string ToQueryString<TEntity>(
this IQueryable<TEntity> query)
where TEntity : class
{
IEnumerator<TEntity>? enumerator = query
.Provider
.Execute<IEnumerable<TEntity>>(query.Expression)
.GetEnumerator();

var relationalCommandCache = enumerator.Private("_relationalCommandCache");

SelectExpression? selectExpression = relationalCommandCache
.Private<SelectExpression>("_selectExpression");

IQuerySqlGeneratorFactory? factory = relationalCommandCache
.Private<IQuerySqlGeneratorFactory>("_querySqlGeneratorFactory");

QuerySqlGenerator? sqlGenerator = factory.Create();
IRelationalCommand? command = sqlGenerator.GetCommand(selectExpression);

return command.CommandText;
}

private static object Private(
this object? obj,
string privateField) =>
obj?.GetType()?
.GetField(privateField, BindingFlags.Instance | BindingFlags.NonPublic)?
.GetValue(obj) ?? throw new InvalidOperationException();

private static T Private<T>(
this object obj,
string privateField)
where T : class =>
(T?)obj?.GetType()?
.GetField(privateField, BindingFlags.Instance | BindingFlags.NonPublic)?
.GetValue(obj) ?? throw new InvalidOperationException();
}
}
@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using HotChocolate.Execution;
using HotChocolate.Resolvers;
using HotChocolate.Types;
using Squadron;

namespace HotChocolate.Data.Projections.Spatial
{
public class ProjectionVisitorTestBase
{
private readonly PostgreSqlResource<PostgisConfig> _resource;

public ProjectionVisitorTestBase(PostgreSqlResource<PostgisConfig> resource)
{
_resource = resource;
}

private async Task<Func<IResolverContext, IEnumerable<T>>> BuildResolverAsync<T>(
params T[] results)
where T : class
{
var databaseName = Guid.NewGuid().ToString("N");
var dbContext = new DatabaseContext<T>(_resource, databaseName);

var sql = dbContext.Database.GenerateCreateScript();
await _resource.CreateDatabaseAsync(databaseName);
await _resource.RunSqlScriptAsync(
"CREATE EXTENSION postgis;\n" + sql,
databaseName);
dbContext.AddRange(results);
dbContext.SaveChanges();

return ctx => dbContext.Data.AsQueryable();
}

protected async Task<IRequestExecutor> CreateSchemaAsync<TEntity>(
TEntity[] entities,
ProjectionConvention? convention = null)
where TEntity : class
{
Func<IResolverContext, IEnumerable<TEntity>> resolver =
await BuildResolverAsync(entities);

return await new ServiceCollection()
.AddGraphQL()
.AddProjections()
.AddSpatialTypes()
.AddSpatialProjections()
.AddQueryType(
c => c
.Name("Query")
.Field("root")
.Resolver(resolver)
.Use(next => async context =>
{
await next(context);
if (context.Result is IQueryable<TEntity> queryable)
{
try
{
context.ContextData["sql"] = queryable.ToQueryString();
}
catch (Exception)
{
context.ContextData["sql"] =
"EF Core 3.1 does not support ToQueryString officially";
}
}
}))
.UseRequest(next => async context =>
{
await next(context);
if (context.Result is IReadOnlyQueryResult result &&
context.ContextData.TryGetValue("sql", out var queryString))
{
context.Result =
QueryResultBuilder
.FromResult(result)
.SetContextData("sql", queryString)
.Create();
}
})
.UseDefaultPipeline()
.BuildRequestExecutorAsync();
}
}
}
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AssemblyName>HotChocolate.Data.Projections.Spatial.SqlServer</AssemblyName>
<RootNamespace>HotChocolate.Data.Projections</RootNamespace>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\Data\src\Data\HotChocolate.Data.csproj"/>
<ProjectReference Include="..\..\..\Spatial\src\Types\HotChocolate.Types.Spatial.csproj"/>
<ProjectReference Include="..\..\..\Spatial\src\Data\HotChocolate.Data.Spatial.csproj"/>
<ProjectReference Include="..\..\..\Core\test\Types.Tests\HotChocolate.Types.Tests.csproj"/>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.0"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="3.1.0"/>
<PackageReference Include="Squadron.PostgreSql" Version="0.8.1"/>
</ItemGroup>

<!--For Visual Studio for Mac Test Explorer we need this reference here-->
<ItemGroup>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>

</Project>
@@ -0,0 +1,17 @@
using System;
using Squadron;

namespace HotChocolate.Data.Projections.Spatial
{
public class PostgisConfig : PostgreSqlDefaultOptions
{
public override void Configure(ContainerResourceBuilder builder) =>
builder
.WaitTimeout(120)
.Name("postgis")
.Image("postgis/postgis:latest")
.Username("postgis")
.Password(Guid.NewGuid().ToString("N").Substring(12))
.InternalPort(5432);
}
}

0 comments on commit 9905252

Please sign in to comment.