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? On { get; set; }
public string? Projected { get; set; }
/// <summary> /// <summary>
/// Create a new <see cref="LoadableRelationAttribute"/>. /// Create a new <see cref="LoadableRelationAttribute"/>.
/// </summary> /// </summary>

View File

@ -126,8 +126,20 @@ namespace Kyoo.Abstractions.Models
/// <summary> /// <summary>
/// The number of episodes in this season. /// The number of episodes in this season.
/// </summary> /// </summary>
[Projectable(UseMemberBody = nameof(_EpisodesCount))] [Projectable(UseMemberBody = nameof(_EpisodesCount), OnlyOnInclude = true)]
[NotMapped] [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; } public int EpisodesCount { get; set; }
private int _EpisodesCount => Episodes!.Count; private int _EpisodesCount => Episodes!.Count;

View File

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

View File

@ -25,17 +25,28 @@ using Kyoo.Abstractions.Models.Attributes;
namespace Kyoo.Abstractions.Models.Utils; namespace Kyoo.Abstractions.Models.Utils;
/// <summary> public class Include
/// The aditional fields to include in the result.
/// </summary>
/// <typeparam name="T">The type related to the new fields</typeparam>
public class Include<T>
{ {
/// <summary> /// <summary>
/// The aditional fields to include in the result. /// The aditional fields to include in the result.
/// </summary> /// </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> /// <summary>
/// The aditional fields names to include in the result. /// The aditional fields names to include in the result.
/// </summary> /// </summary>
@ -79,16 +90,12 @@ public class Include<T>
// } // }
if (attr.Sql != null) if (attr.Sql != null)
return new CustomRelation(prop.Name, prop.PropertyType, attr.Sql, attr.On, prop.DeclaringType!); 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(); throw new NotImplementedException();
}) })
.Distinct(); .Distinct();
}).ToArray() }).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 InterpolatedSql.SqlBuilders;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Utils; using Kyoo.Abstractions.Models.Utils;
using Kyoo.Utils; using Kyoo.Utils;
@ -103,7 +104,7 @@ namespace Kyoo.Core.Controllers
IEnumerable<string> keys = config IEnumerable<string> keys = config
.Where(x => key == "id" || x.Value.GetProperty(key) != null) .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)})"; return $"coalesce({string.Join(", ", keys)})";
} }
@ -140,12 +141,12 @@ namespace Kyoo.Core.Controllers
relation++; relation++;
switch (metadata) 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"; string tableName = type.GetCustomAttribute<TableAttribute>()?.Name ?? $"{type.Name.ToSnakeCase()}s";
retConfig.Add($"r{relation}", type); retConfig.Add($"r{relation}", type);
join.AppendLine($"left join {tableName} as r{relation} on r{relation}.id = {_Property(rid, config)}"); join.AppendLine($"left join {tableName} as r{relation} on r{relation}.id = {_Property(rid, config)}");
break; 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 owner = config.First(x => x.Value == declaring).Key;
string lateral = sql.Contains("\"this\"") ? " lateral" : string.Empty; string lateral = sql.Contains("\"this\"") ? " lateral" : string.Empty;
sql = sql.Replace("\"this\"", owner); sql = sql.Replace("\"this\"", owner);
@ -153,6 +154,8 @@ namespace Kyoo.Core.Controllers
retConfig.Add($"r{relation}", type); retConfig.Add($"r{relation}", type);
join.AppendLine($"left join{lateral} ({sql}) as r{relation} on r{relation}.{on}"); join.AppendLine($"left join{lateral} ({sql}) as r{relation} on r{relation}.{on}");
break; break;
case Include.ProjectedRelation:
continue;
default: default:
throw new NotImplementedException(); throw new NotImplementedException();
} }
@ -174,6 +177,17 @@ namespace Kyoo.Core.Controllers
return (retConfig, join.ToString(), Map); 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( public async Task<ICollection<ILibraryItem>> GetAll(
Expression<Func<ILibraryItem, bool>>? where = null, Expression<Func<ILibraryItem, bool>>? where = null,
Sort<ILibraryItem>? sort = null, Sort<ILibraryItem>? sort = null,
@ -191,7 +205,7 @@ namespace Kyoo.Core.Controllers
// language=PostgreSQL // language=PostgreSQL
IDapperSqlCommand query = _database.SqlBuilder($""" IDapperSqlCommand query = _database.SqlBuilder($"""
select select
s.*, {ExpendProjections<Show>("s", include):raw},
m.*, m.*,
c.* c.*
{string.Join(string.Empty, includeConfig.Select(x => $", {x.Key}.*")):raw} {string.Join(string.Empty, includeConfig.Select(x => $", {x.Key}.*")):raw}
@ -199,12 +213,12 @@ namespace Kyoo.Core.Controllers
shows as s shows as s
full outer join ( full outer join (
select select
* {ExpendProjections<Movie>(null, include):raw}
from from
movies) as m on false movies) as m on false
full outer join ( full outer join (
select select
* {ExpendProjections<Collection>(null, include):raw}
from from
collections) as c on false collections) as c on false
{includeJoin:raw} {includeJoin:raw}

View File

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

View File

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