Skip to content

Wire up HighNeedsService.Get() to underlying database views #2113

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 14, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ AS
IIF(Population2To18 > 0.0, OutturnPlaceFundingPrimary / Population2To18, NULL) AS OutturnPlaceFundingPrimary,
IIF(Population2To18 > 0.0, OutturnPlaceFundingSecondary / Population2To18, NULL) AS OutturnPlaceFundingSecondary,
IIF(Population2To18 > 0.0, OutturnPlaceFundingSpecial / Population2To18, NULL) AS OutturnPlaceFundingSpecial,
IIF(Population2To18 > 0.0, OutturnPlaceFundingAlternativeProvision / Population2To18, NULL) AS TotalExOutturnPlaceFundingAlternativeProvisionpenditure,
IIF(Population2To18 > 0.0, OutturnPlaceFundingAlternativeProvision / Population2To18, NULL) AS OutturnPlaceFundingAlternativeProvision,
IIF(Population2To18 > 0.0, BudgetTotalHighNeeds / Population2To18, NULL) AS BudgetTotalHighNeeds,
IIF(Population2To18 > 0.0, BudgetTotalPlaceFunding / Population2To18, NULL) AS BudgetTotalPlaceFunding,
IIF(Population2To18 > 0.0, BudgetTotalTopUpFundingMaintained / Population2To18, NULL) AS BudgetTotalTopUpFundingMaintained,
Expand Down
99 changes: 58 additions & 41 deletions platform/src/abstractions/Platform.Sql/DatabaseConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,27 @@ public Task<IEnumerable<T>> QueryAsync<T>(string sql, object? param = null, Canc
public Task<IEnumerable<T>> QueryAsync<T>(PlatformQuery query, CancellationToken cancellationToken = default)
=> connection.QueryAsync<T>(new CommandDefinition(query.QueryTemplate.RawSql, query.QueryTemplate.Parameters, cancellationToken: cancellationToken));

public Task<IEnumerable<TReturn>> QueryAsync<TFirst, TSecond, TReturn>(PlatformQuery query, Func<TFirst, TSecond, TReturn> map, string[] splitOn, CancellationToken cancellationToken = default) =>
connection.QueryAsync(new CommandDefinition(query.QueryTemplate.RawSql, query.QueryTemplate.Parameters, cancellationToken: cancellationToken), map, string.Join(", ", splitOn));

public Task<IEnumerable<TReturn>> QueryAsync<TFirst, TSecond, TThird, TReturn>(PlatformQuery query, Func<TFirst, TSecond, TThird, TReturn> map, string[] splitOn, CancellationToken cancellationToken = default) =>
connection.QueryAsync(new CommandDefinition(query.QueryTemplate.RawSql, query.QueryTemplate.Parameters, cancellationToken: cancellationToken), map, string.Join(", ", splitOn));

public Task<IEnumerable<TReturn>> QueryAsync<TFirst, TSecond, TThird, TFourth, TReturn>(PlatformQuery query, Func<TFirst, TSecond, TThird, TFourth, TReturn> map, string[] splitOn, CancellationToken cancellationToken = default) =>
connection.QueryAsync(new CommandDefinition(query.QueryTemplate.RawSql, query.QueryTemplate.Parameters, cancellationToken: cancellationToken), map, string.Join(", ", splitOn));

public Task<IEnumerable<TReturn>> QueryAsync<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(PlatformQuery query, Func<TFirst, TSecond, TThird, TFourth, TFifth, TReturn> map, string[] splitOn, CancellationToken cancellationToken = default) =>
connection.QueryAsync(new CommandDefinition(query.QueryTemplate.RawSql, query.QueryTemplate.Parameters, cancellationToken: cancellationToken), map, string.Join(", ", splitOn));

public Task<IEnumerable<TReturn>> QueryAsync<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TReturn>(PlatformQuery query, Func<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TReturn> map, string[] splitOn, CancellationToken cancellationToken = default) =>
connection.QueryAsync(new CommandDefinition(query.QueryTemplate.RawSql, query.QueryTemplate.Parameters, cancellationToken: cancellationToken), map, string.Join(", ", splitOn));

public Task<IEnumerable<TReturn>> QueryAsync<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TSeventh, TReturn>(PlatformQuery query, Func<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TSeventh, TReturn> map, string[] splitOn, CancellationToken cancellationToken = default) =>
connection.QueryAsync(new CommandDefinition(query.QueryTemplate.RawSql, query.QueryTemplate.Parameters, cancellationToken: cancellationToken), map, string.Join(", ", splitOn));

public Task<IEnumerable<TReturn>> QueryAsync<TReturn>(PlatformQuery query, Type[] types, Func<object[], TReturn> map, string[] splitOn) =>
connection.QueryAsync(query.QueryTemplate.RawSql, types, map, query.QueryTemplate.Parameters, splitOn: string.Join(", ", splitOn));

public Task<T?> QueryFirstOrDefaultAsync<T>(string sql, object? param = null, CancellationToken cancellationToken = default)
=> connection.QueryFirstOrDefaultAsync<T>(new CommandDefinition(sql, param, cancellationToken: cancellationToken));

Expand All @@ -73,58 +94,54 @@ public Task<int> InsertAsync<T>(T entityToInsert, IDbTransaction? transaction =

public interface IDatabaseConnection : IDbConnection
{
/// <summary>
/// Execute a query asynchronously using Task.
/// </summary>
/// <typeparam name="T">The type of results to return.</typeparam>
/// <param name="sql">The SQL to execute for the query.</param>
/// <param name="param">The parameters to pass, if any.</param>
/// <param name="cancellationToken">The cancellation token for this command.</param>
/// <returns>
/// A sequence of data of <typeparamref name="T" />; if a basic type (int, string, etc) is queried then the data from
/// the first column in assumed, otherwise an instance is
/// created per row, and a direct column-name===member-name mapping is assumed (case insensitive).
/// </returns>
/// <inheritdoc cref="Dapper.SqlMapper.QueryAsync&lt;T&gt;(IDbConnection, CommandDefinition)" />
Task<IEnumerable<T>> QueryAsync<T>(string sql, object? param = null, CancellationToken cancellationToken = default);

/// <inheritdoc cref="Dapper.SqlMapper.QueryAsync&lt;T&gt;(IDbConnection, CommandDefinition)" />
Task<IEnumerable<T>> QueryAsync<T>(PlatformQuery query, CancellationToken cancellationToken = default);

/// <summary>
/// Execute a single-row query asynchronously using Task.
/// </summary>
/// <typeparam name="T">The type of result to return.</typeparam>
/// <param name="sql">The SQL to execute for the query.</param>
/// <param name="param">The parameters to pass, if any.</param>
/// <param name="cancellationToken">The cancellation token for this command.</param>
/// <inheritdoc
/// cref="Dapper.SqlMapper.QueryAsync&lt;TFirst,TSecond,TReturn&gt;(IDbConnection, CommandDefinition, Func&lt;TFirst,TSecond,TReturn&gt;, string)" />
Task<IEnumerable<TReturn>> QueryAsync<TFirst, TSecond, TReturn>(PlatformQuery query, Func<TFirst, TSecond, TReturn> map, string[] splitOn, CancellationToken cancellationToken = default);

/// <inheritdoc
/// cref="Dapper.SqlMapper.QueryAsync&lt;TFirst,TSecond,TThird,TReturn&gt;(IDbConnection, CommandDefinition, Func&lt;TFirst,TSecond,TThird,TReturn&gt;, string)" />
Task<IEnumerable<TReturn>> QueryAsync<TFirst, TSecond, TThird, TReturn>(PlatformQuery query, Func<TFirst, TSecond, TThird, TReturn> map, string[] splitOn, CancellationToken cancellationToken = default);

/// <inheritdoc
/// cref="Dapper.SqlMapper.QueryAsync&lt;TFirst,TSecond,TThird,TFourth,TReturn&gt;(IDbConnection, CommandDefinition, Func&lt;TFirst,TSecond,TThird,TFourth,TReturn&gt;, string)" />
Task<IEnumerable<TReturn>> QueryAsync<TFirst, TSecond, TThird, TFourth, TReturn>(PlatformQuery query, Func<TFirst, TSecond, TThird, TFourth, TReturn> map, string[] splitOn, CancellationToken cancellationToken = default);

/// <inheritdoc
/// cref="Dapper.SqlMapper.QueryAsync&lt;TFirst,TSecond,TThird,TFourth,TFifth,TReturn&gt;(IDbConnection, CommandDefinition, Func&lt;TFirst,TSecond,TThird,TFourth,TFifth,TReturn&gt;, string)" />
Task<IEnumerable<TReturn>> QueryAsync<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(PlatformQuery query, Func<TFirst, TSecond, TThird, TFourth, TFifth, TReturn> map, string[] splitOn, CancellationToken cancellationToken = default);

/// <inheritdoc
/// cref="Dapper.SqlMapper.QueryAsync&lt;TFirst,TSecond,TThird,TFourth,TFifth,TSixth,TReturn&gt;(IDbConnection, CommandDefinition, Func&lt;TFirst,TSecond,TThird,TFourth,TFifth,TSixth,TReturn&gt;, string)" />
Task<IEnumerable<TReturn>> QueryAsync<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TReturn>(PlatformQuery query, Func<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TReturn> map, string[] splitOn, CancellationToken cancellationToken = default);

/// <inheritdoc
/// cref="Dapper.SqlMapper.QueryAsync&lt;TFirst,TSecond,TThird,TFourth,TFifth,TSixth,TSeventh,TReturn&gt;(IDbConnection, CommandDefinition, Func&lt;TFirst,TSecond,TThird,TFourth,TFifth,TSixth,TSeventh,TReturn&gt;, string)" />
Task<IEnumerable<TReturn>> QueryAsync<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TSeventh, TReturn>(PlatformQuery query, Func<TFirst, TSecond, TThird, TFourth, TFifth, TSixth, TSeventh, TReturn> map, string[] splitOn, CancellationToken cancellationToken = default);

/// <inheritdoc
/// cref="Dapper.SqlMapper.QueryAsync&lt;TReturn&gt;(IDbConnection, string, Type[], Func&lt;object[], TReturn&gt;, object?, IDbTransaction?, bool, string, int?, CommandType?)" />
/// <remarks>No <see cref="CancellationToken" /> support (see https://github.com/DapperLib/Dapper/issues/2125).</remarks>
Task<IEnumerable<TReturn>> QueryAsync<TReturn>(PlatformQuery query, Type[] types, Func<object[], TReturn> map, string[] splitOn);

/// <inheritdoc cref="Dapper.SqlMapper.QueryFirstOrDefaultAsync&lt;T&gt;(IDbConnection, CommandDefinition)" />
Task<T?> QueryFirstOrDefaultAsync<T>(string sql, object? param = null, CancellationToken cancellationToken = default);

/// <inheritdoc cref="Dapper.SqlMapper.QueryFirstOrDefaultAsync&lt;T&gt;(IDbConnection, CommandDefinition)" />
Task<T?> QueryFirstOrDefaultAsync<T>(PlatformQuery query, CancellationToken cancellationToken = default);

/// <summary>
/// Execute a command asynchronously using Task.
/// </summary>
/// <param name="sql">The SQL to execute for this query.</param>
/// <param name="param">The parameters to use for this query.</param>
/// <param name="transaction">The transaction to use for this query.</param>
/// <param name="cancellationToken">The cancellation token for this command.</param>
/// <returns>The number of rows affected.</returns>
/// <inheritdoc cref="Dapper.SqlMapper.ExecuteAsync(IDbConnection, CommandDefinition)" />
Task<int> ExecuteAsync(string sql, object? param = null, IDbTransaction? transaction = null, CancellationToken cancellationToken = default);

/// <summary>
/// Execute a single-row query asynchronously using Task.
/// </summary>
/// <typeparam name="T">The type of result to return.</typeparam>
/// <param name="sql">The SQL to execute for the query.</param>
/// <param name="param">The parameters to pass, if any.</param>
/// <param name="cancellationToken">The cancellation token for this command.</param>
/// <inheritdoc cref="Dapper.SqlMapper.QueryFirstAsync&lt;T&gt;(IDbConnection, CommandDefinition)" />
Task<T> QueryFirstAsync<T>(string sql, object? param = null, CancellationToken cancellationToken = default);

/// <summary>
/// Inserts an entity into table "Ts" asynchronously using Task and returns identity id.
/// </summary>
/// <typeparam name="T">The type being inserted.</typeparam>
/// <param name="entityToInsert">Entity to insert</param>
/// <param name="transaction">The transaction to run under, null (the default) if none</param>
/// <returns>Identity of inserted entity</returns>
/// <inheritdoc
/// cref="Dapper.Contrib.Extensions.SqlMapperExtensions.InsertAsync&lt;T&gt;(IDbConnection, T, IDbTransaction, int?, ISqlAdapter)" />
Task<int> InsertAsync<T>(T entityToInsert, IDbTransaction? transaction = null) where T : class;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Platform.Sql.QueryBuilders;
using Platform.Domain;

namespace Platform.Sql.QueryBuilders;

public class LocalAuthorityQuery() : PlatformQuery(Sql)
{
Expand All @@ -8,4 +10,26 @@ public class LocalAuthorityQuery() : PlatformQuery(Sql)
public class LocalAuthorityStatisticalNeighbourQuery() : PlatformQuery(Sql)
{
private const string Sql = "SELECT * FROM VW_LocalAuthorityStatisticalNeighbours /**where**/";
}

public class LocalAuthorityCurrentFinancialQuery : PlatformQuery
{
public LocalAuthorityCurrentFinancialQuery(string dimension, string[] fields) : base(GetSql(dimension, fields))
{
foreach (var field in fields)
{
Select(field);
}
}

private static string GetSql(string dimension, string[] fields)
{
var select = fields.Length == 0 ? "*" : "/**select**/";
return dimension switch
{
Dimensions.HighNeeds.Actuals => $"SELECT {select} FROM VW_LocalAuthorityFinancialDefaultCurrentActual /**where**/",
Dimensions.HighNeeds.PerHead => $"SELECT {select} FROM VW_LocalAuthorityFinancialDefaultCurrentPerPopulation /**where**/",
_ => throw new ArgumentOutOfRangeException(nameof(dimension), "Unknown dimension")
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ public PlatformQuery WhereLaCodeEqual(string laCode)
return this;
}

public PlatformQuery WhereLaCodesIn(string[] laCodes)
{
const string sql = "LaCode IN @LaCodes";
var parameters = new { LaCodes = laCodes };

Where(sql, parameters);
return this;
}

public PlatformQuery WhereCodeEqual(string code)
{
const string sql = "Code = @Code";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,31 @@ public void ShouldReturnSql()
}

private static LocalAuthorityQuery Create() => new();
}

public class LocalAuthorityCurrentFinancialQueryTests
{
[Theory]
[MemberData(nameof(Data))]
public void ShouldReturnSql(string dimension, string[] fields, string expected)
{
var builder = Create(dimension, fields);
Assert.Equal(expected, builder.QueryTemplate.RawSql);
}

[Fact]
public void ShouldThrowArgumentOutOfRangeException()
{
Assert.Throws<ArgumentOutOfRangeException>(() => Create("dimension", []));
}

public static TheoryData<string, string[], string> Data => new()
{
{ "Actuals", [], "SELECT * FROM VW_LocalAuthorityFinancialDefaultCurrentActual " },
{ "Actuals", ["field1", "field2"], "SELECT field1 , field2\n FROM VW_LocalAuthorityFinancialDefaultCurrentActual " },
{ "PerHead", [], "SELECT * FROM VW_LocalAuthorityFinancialDefaultCurrentPerPopulation " },
{ "PerHead", ["field1", "field2"], "SELECT field1 , field2\n FROM VW_LocalAuthorityFinancialDefaultCurrentPerPopulation " },
};

private static LocalAuthorityCurrentFinancialQuery Create(string dimension, string[] fields) => new(dimension, fields);
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,22 @@ public void ShouldAddLaCodeEqualParameter()
Assert.Equal(expectedSql, builder.QueryTemplate.RawSql);
}

[Fact]
public void ShouldAddLaCodeInParameter()
{
const string expectedParam = "LaCodes";
var expectedValue = new[] { "12345", "12346" };
var expectedSql = BuildExpectedQuery("WHERE LaCode IN @LaCodes");

var builder = new MockPlatformQuery().WhereLaCodesIn(expectedValue);
var parameters = builder.QueryTemplate.Parameters.GetTemplateParameters(expectedParam);

Assert.Single(parameters);
Assert.Contains(expectedParam, parameters.Keys);
Assert.Equal(expectedValue, parameters[expectedParam]);
Assert.Equal(expectedSql, builder.QueryTemplate.RawSql);
}

[Fact]
public void ShouldAddOverallPhaseEqualParameter()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Platform.Api.Establishment.Features.LocalAuthorities.Models;
using Platform.Api.Establishment.Features.LocalAuthorities.Requests;
using Platform.Domain;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public static class HighNeedsFeature
public static IServiceCollection AddHighNeedsFeature(this IServiceCollection serviceCollection)
{
serviceCollection
.AddSingleton<IHighNeedsService, HighNeedsStubService>();
.AddSingleton<IHighNeedsService, HighNeedsService>();

serviceCollection
.AddTransient<IValidator<HighNeedsParameters>, HighNeedsParametersValidator>()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Platform.Api.LocalAuthorityFinances.Features.HighNeeds.Models;

namespace Platform.Api.LocalAuthorityFinances.Features.HighNeeds;

public static class Mapper
{
public static LocalAuthority<Models.HighNeeds> MultiMapToHighNeeds(object[] objects)
{
var localAuthority = objects[0] as LocalAuthorityBase;
var outturn = objects[1] as HighNeedsBase;
var outturnHighNeedsAmount = objects[2] as HighNeedsAmount;
var outturnTopFundingMaintained = objects[3] as TopFunding;
var outturnTopFundingNonMaintained = objects[4] as TopFunding;
var outturnPlaceFunding = objects[5] as PlaceFunding;
var budget = objects[6] as HighNeedsBase;
var budgetHighNeedsAmount = objects[7] as HighNeedsAmount;
var budgetTopFundingMaintained = objects[8] as TopFunding;
var budgetTopFundingNonMaintained = objects[9] as TopFunding;
var budgetPlaceFunding = objects[10] as PlaceFunding;

return new LocalAuthority<Models.HighNeeds>
{
Code = localAuthority?.Code,
Name = localAuthority?.Name,
Outturn = new Models.HighNeeds
{
Total = outturn?.Total,
HighNeedsAmount = outturnHighNeedsAmount,
Maintained = outturnTopFundingMaintained,
NonMaintained = outturnTopFundingNonMaintained,
PlaceFunding = outturnPlaceFunding
},
Budget = new Models.HighNeeds
{
Total = budget?.Total,
HighNeedsAmount = budgetHighNeedsAmount,
Maintained = budgetTopFundingMaintained,
NonMaintained = budgetTopFundingNonMaintained,
PlaceFunding = budgetPlaceFunding
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@
// ReSharper disable UnusedAutoPropertyAccessor.Global
namespace Platform.Api.LocalAuthorityFinances.Features.HighNeeds.Models;

public record HighNeeds
public record HighNeeds : HighNeedsBase
{
public HighNeedsAmount? HighNeedsAmount { get; set; }
public TopFunding? Maintained { get; set; }
public TopFunding? NonMaintained { get; set; }
public PlaceFunding? PlaceFunding { get; set; }
}

public record HighNeedsBase
{
public decimal? Total { get; set; }
public HighNeedsAmount HighNeedsAmount { get; set; } = new();
public TopFunding Maintained { get; set; } = new();
public TopFunding NonMaintained { get; set; } = new();
public PlaceFunding PlaceFunding { get; set; } = new();
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
// ReSharper disable UnusedAutoPropertyAccessor.Global
namespace Platform.Api.LocalAuthorityFinances.Features.HighNeeds.Models;

public record LocalAuthority<T>
public record LocalAuthority<T> : LocalAuthorityBase
{
public string? Code { get; set; }
public string? Name { get; set; }
public T? Outturn { get; set; }
public T? Budget { get; set; }
}

public record LocalAuthorityBase
{
public string? Code { get; set; }
public string? Name { get; set; }
}
Loading