Add projected relations

This commit is contained in:
Zoe Roux 2023-11-21 23:14:43 +01:00
parent 0ff03fb413
commit e8351e960d
7 changed files with 60 additions and 21 deletions

View File

@ -35,6 +35,8 @@ namespace Kyoo.Abstractions.Models.Attributes
public string? On { get; set; }
public string? Projected { get; set; }
/// <summary>
/// Create a new <see cref="LoadableRelationAttribute"/>.
/// </summary>

View File

@ -126,8 +126,20 @@ namespace Kyoo.Abstractions.Models
/// <summary>
/// The number of episodes in this season.
/// </summary>
[Projectable(UseMemberBody = nameof(_EpisodesCount))]
[Projectable(UseMemberBody = nameof(_EpisodesCount), OnlyOnInclude = true)]
[NotMapped]
[LoadableRelation(
// language=PostgreSQL
Projected = """
(
select
count(*)::int
from
episodes as e
where
e.season_id = id) as episode_count
"""
)]
public int EpisodesCount { get; set; }
private int _EpisodesCount => Episodes!.Count;

View File

@ -19,6 +19,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using EntityFrameworkCore.Projectables;
using Kyoo.Abstractions.Controllers;
@ -111,6 +112,7 @@ namespace Kyoo.Abstractions.Models
public string? Trailer { get; set; }
[SerializeIgnore]
[Column("start_air")]
public DateTime? AirDate => StartAir;
/// <inheritdoc />

View File

@ -25,17 +25,28 @@ using Kyoo.Abstractions.Models.Attributes;
namespace Kyoo.Abstractions.Models.Utils;
/// <summary>
/// The aditional fields to include in the result.
/// </summary>
/// <typeparam name="T">The type related to the new fields</typeparam>
public class Include<T>
public class Include
{
/// <summary>
/// The aditional fields to include in the result.
/// </summary>
public ICollection<Metadata> Metadatas { get; private init; } = ArraySegment<Metadata>.Empty;
public ICollection<Metadata> Metadatas { get; init; } = ArraySegment<Metadata>.Empty;
public abstract record Metadata(string Name);
public record SingleRelation(string Name, Type type, string RelationIdName) : Metadata(Name);
public record CustomRelation(string Name, Type type, string Sql, string? On, Type Declaring) : Metadata(Name);
public record ProjectedRelation(string Name, string Sql) : Metadata(Name);
}
/// <summary>
/// The aditional fields to include in the result.
/// </summary>
/// <typeparam name="T">The type related to the new fields</typeparam>
public class Include<T> : Include
{
/// <summary>
/// The aditional fields names to include in the result.
/// </summary>
@ -79,16 +90,12 @@ public class Include<T>
// }
if (attr.Sql != null)
return new CustomRelation(prop.Name, prop.PropertyType, attr.Sql, attr.On, prop.DeclaringType!);
if (attr.Projected != null)
return new ProjectedRelation(prop.Name, attr.Projected);
throw new NotImplementedException();
})
.Distinct();
}).ToArray()
};
}
public abstract record Metadata(string Name);
public record SingleRelation(string Name, Type type, string RelationIdName) : Metadata(Name);
public record CustomRelation(string Name, Type type, string Sql, string? On, Type Declaring) : Metadata(Name);
}

View File

@ -32,6 +32,7 @@ using InterpolatedSql.Dapper;
using InterpolatedSql.SqlBuilders;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Utils;
using Kyoo.Utils;
@ -103,7 +104,7 @@ namespace Kyoo.Core.Controllers
IEnumerable<string> keys = config
.Where(x => key == "id" || x.Value.GetProperty(key) != null)
.Select(x => $"{x.Key}.{key.ToSnakeCase()}");
.Select(x => $"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute<ColumnAttribute>()?.Name ?? key.ToSnakeCase()}");
return $"coalesce({string.Join(", ", keys)})";
}
@ -140,12 +141,12 @@ namespace Kyoo.Core.Controllers
relation++;
switch (metadata)
{
case Include<T>.SingleRelation(var name, var type, var rid):
case Include.SingleRelation(var name, var type, var rid):
string tableName = type.GetCustomAttribute<TableAttribute>()?.Name ?? $"{type.Name.ToSnakeCase()}s";
retConfig.Add($"r{relation}", type);
join.AppendLine($"left join {tableName} as r{relation} on r{relation}.id = {_Property(rid, config)}");
break;
case Include<T>.CustomRelation(var name, var type, var sql, var on, var declaring):
case Include.CustomRelation(var name, var type, var sql, var on, var declaring):
string owner = config.First(x => x.Value == declaring).Key;
string lateral = sql.Contains("\"this\"") ? " lateral" : string.Empty;
sql = sql.Replace("\"this\"", owner);
@ -153,6 +154,8 @@ namespace Kyoo.Core.Controllers
retConfig.Add($"r{relation}", type);
join.AppendLine($"left join{lateral} ({sql}) as r{relation} on r{relation}.{on}");
break;
case Include.ProjectedRelation:
continue;
default:
throw new NotImplementedException();
}
@ -174,6 +177,17 @@ namespace Kyoo.Core.Controllers
return (retConfig, join.ToString(), Map);
}
public static string ExpendProjections<T>(string? prefix, Include include)
{
prefix = prefix != null ? $"{prefix}." : string.Empty;
IEnumerable<string> projections = include.Metadatas
.Select(x => x is Include.ProjectedRelation(var name, var sql) ? sql : null!)
.Where(x => x != null)
.Select(x => x.Replace("\"this\".", prefix));
string projStr = string.Join(string.Empty, projections.Select(x => $", {x}"));
return $"{prefix}*" + projStr;
}
public async Task<ICollection<ILibraryItem>> GetAll(
Expression<Func<ILibraryItem, bool>>? where = null,
Sort<ILibraryItem>? sort = null,
@ -191,7 +205,7 @@ namespace Kyoo.Core.Controllers
// language=PostgreSQL
IDapperSqlCommand query = _database.SqlBuilder($"""
select
s.*,
{ExpendProjections<Show>("s", include):raw},
m.*,
c.*
{string.Join(string.Empty, includeConfig.Select(x => $", {x.Key}.*")):raw}
@ -199,12 +213,12 @@ namespace Kyoo.Core.Controllers
shows as s
full outer join (
select
*
{ExpendProjections<Movie>(null, include):raw}
from
movies) as m on false
full outer join (
full outer join (
select
*
{ExpendProjections<Collection>(null, include):raw}
from
collections) as c on false
{includeJoin:raw}

View File

@ -245,7 +245,8 @@ namespace Kyoo.Postgresql
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Show>()
.Ignore(x => x.FirstEpisode);
.Ignore(x => x.FirstEpisode)
.Ignore(x => x.AirDate);
modelBuilder.Entity<Episode>()
.Ignore(x => x.PreviousEpisode)
.Ignore(x => x.NextEpisode);

View File

@ -94,6 +94,7 @@ SeasonHeader.query = (slug: string): QueryIdentifier<Season, SeasonProcessed> =>
params: {
// Fetch all seasons at one, there won't be hundred of thems anyways.
limit: 0,
fields: ["episodesCount"],
},
infinite: {
value: true,