diff --git a/.pg_format b/.pg_format
index 5d346b91..882260a2 100644
--- a/.pg_format
+++ b/.pg_format
@@ -2,3 +2,4 @@ tabs=1
function-case=1 #lowercase
keyword-case=1
type-case=1
+no-space-function=1
diff --git a/back/src/Directory.Build.props b/back/src/Directory.Build.props
index 53bd9b50..9934d2e0 100644
--- a/back/src/Directory.Build.props
+++ b/back/src/Directory.Build.props
@@ -46,7 +46,7 @@
$(MSBuildThisFileDirectory)../Kyoo.ruleset
- 1591;1305;8618
+ 1591;1305;8618;SYSLIB1045
diff --git a/back/src/Kyoo.Abstractions/Controllers/IRepository.cs b/back/src/Kyoo.Abstractions/Controllers/IRepository.cs
index e3fea697..c6300930 100644
--- a/back/src/Kyoo.Abstractions/Controllers/IRepository.cs
+++ b/back/src/Kyoo.Abstractions/Controllers/IRepository.cs
@@ -113,7 +113,7 @@ namespace Kyoo.Abstractions.Controllers
Task> GetAll(Filter? filter = null,
Sort? sort = default,
Include? include = default,
- Pagination limit = default);
+ Pagination? limit = default);
///
/// Get the number of resources that match the filter's predicate.
diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs b/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs
index d46f32e5..de0deb0a 100644
--- a/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs
+++ b/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs
@@ -174,7 +174,7 @@ namespace Kyoo.Abstractions.Models
// language=PostgreSQL
Sql = """
select
- "pe".*,
+ "pe".* -- Episode as pe
from
episodes as "pe"
where
@@ -210,7 +210,7 @@ namespace Kyoo.Abstractions.Models
// language=PostgreSQL
Sql = """
select
- "ne".*,
+ "ne".* -- Episode as ne
from
episodes as "ne"
where
diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Show.cs b/back/src/Kyoo.Abstractions/Models/Resources/Show.cs
index d6dd84e2..4894731b 100644
--- a/back/src/Kyoo.Abstractions/Models/Resources/Show.cs
+++ b/back/src/Kyoo.Abstractions/Models/Resources/Show.cs
@@ -158,7 +158,7 @@ namespace Kyoo.Abstractions.Models
// language=PostgreSQL
Sql = """
select
- "fe".*
+ "fe".* -- Episode as fe
from (
select
e.*,
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs b/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs
new file mode 100644
index 00000000..f1776144
--- /dev/null
+++ b/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs
@@ -0,0 +1,245 @@
+// Kyoo - A portable and vast media library solution.
+// Copyright (c) Kyoo.
+//
+// See AUTHORS.md and LICENSE file in the project root for full license information.
+//
+// Kyoo is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// any later version.
+//
+// Kyoo is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Kyoo. If not, see .
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Data;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Dapper;
+using InterpolatedSql.Dapper;
+using Kyoo.Abstractions.Controllers;
+using Kyoo.Abstractions.Models;
+using Kyoo.Abstractions.Models.Utils;
+using Kyoo.Utils;
+
+namespace Kyoo.Core.Controllers;
+
+public static class DapperHelper
+{
+ private static string _Property(string key, Dictionary config)
+ {
+ if (config.Count == 1)
+ return $"{config.First()}.{key.ToSnakeCase()}";
+
+ IEnumerable keys = config
+ .Where(x => key == "id" || x.Value.GetProperty(key) != null)
+ .Select(x => $"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute()?.Name ?? key.ToSnakeCase()}");
+ return $"coalesce({string.Join(", ", keys)})";
+ }
+
+ public static string ProcessSort(Sort? sort, bool reverse, Dictionary config, bool recurse = false)
+ where T : IQuery
+ {
+ sort ??= new Sort.Default();
+
+ string ret = sort switch
+ {
+ Sort.Default(var value) => ProcessSort(value, reverse, config, true),
+ Sort.By(string key, bool desc) => $"{_Property(key, config)} {(desc ^ reverse ? "desc" : "asc")}",
+ Sort.Random(var seed) => $"md5('{seed}' || {_Property("id", config)}) {(reverse ? "desc" : "asc")}",
+ Sort.Conglomerate(var list) => string.Join(", ", list.Select(x => ProcessSort(x, reverse, config, true))),
+ _ => throw new SwitchExpressionException(),
+ };
+ if (recurse)
+ return ret;
+ // always end query by an id sort.
+ return $"{ret}, {_Property("id", config)} {(reverse ? "desc" : "asc")}";
+ }
+
+ public static (
+ Dictionary config,
+ string join,
+ Func, T> map
+ ) ProcessInclude(Include include, Dictionary config)
+ where T : class
+ {
+ int relation = 0;
+ Dictionary retConfig = new();
+ StringBuilder join = new();
+
+ foreach (Include.Metadata metadata in include.Metadatas)
+ {
+ relation++;
+ switch (metadata)
+ {
+ case Include.SingleRelation(var name, var type, var rid):
+ string tableName = type.GetCustomAttribute()?.Name ?? $"{type.Name.ToSnakeCase()}s";
+ retConfig.Add($"r{relation}", type);
+ join.Append($"\nleft join {tableName} as r{relation} on r{relation}.id = {_Property(rid, config)}");
+ break;
+ 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);
+ on = on?.Replace("\"this\"", owner);
+ retConfig.Add($"r{relation}", type);
+ join.Append($"\nleft join{lateral} ({sql}) as r{relation} on r{relation}.{on}");
+ break;
+ case Include.ProjectedRelation:
+ continue;
+ default:
+ throw new NotImplementedException();
+ }
+ }
+
+ T Map(T item, IEnumerable