Fix random sort after id and implement dapper from ids

This commit is contained in:
Zoe Roux 2023-11-29 01:46:27 +01:00
parent 29d846a944
commit 0ed35e0354
6 changed files with 41 additions and 20 deletions

View File

@ -100,7 +100,7 @@ public abstract record Filter<T> : Filter
/// Internal filter used for keyset paginations to resume random sorts. /// Internal filter used for keyset paginations to resume random sorts.
/// The pseudo sql is md5(seed || table.id) = md5(seed || 'hardCodedId') /// The pseudo sql is md5(seed || table.id) = md5(seed || 'hardCodedId')
/// </summary> /// </summary>
public record EqRandom(string Seed, Guid ReferenceId) : Filter<T>; public record CmpRandom(string cmp, string Seed, Guid ReferenceId) : Filter<T>;
/// <summary> /// <summary>
/// Internal filter used only in EF with hard coded lamdas (used for relations). /// Internal filter used only in EF with hard coded lamdas (used for relations).

View File

@ -37,7 +37,7 @@ namespace Kyoo.Core.Controllers;
public static class DapperHelper public static class DapperHelper
{ {
private static string _Property(string key, Dictionary<string, Type> config) public static string Property(string key, Dictionary<string, Type> config)
{ {
if (key == "kind") if (key == "kind")
return "kind"; return "kind";
@ -56,15 +56,15 @@ public static class DapperHelper
string ret = sort switch string ret = sort switch
{ {
Sort<T>.Default(var value) => ProcessSort(value, reverse, config, true), Sort<T>.Default(var value) => ProcessSort(value, reverse, config, true),
Sort<T>.By(string key, bool desc) => $"{_Property(key, config)} {(desc ^ reverse ? "desc" : "asc")}", Sort<T>.By(string key, bool desc) => $"{Property(key, config)} {(desc ^ reverse ? "desc" : "asc")}",
Sort<T>.Random(var seed) => $"md5('{seed}' || {_Property("id", config)}) {(reverse ? "desc" : "asc")}", Sort<T>.Random(var seed) => $"md5('{seed}' || {Property("id", config)}) {(reverse ? "desc" : "asc")}",
Sort<T>.Conglomerate(var list) => string.Join(", ", list.Select(x => ProcessSort(x, reverse, config, true))), Sort<T>.Conglomerate(var list) => string.Join(", ", list.Select(x => ProcessSort(x, reverse, config, true))),
_ => throw new SwitchExpressionException(), _ => throw new SwitchExpressionException(),
}; };
if (recurse) if (recurse)
return ret; return ret;
// always end query by an id sort. // always end query by an id sort.
return $"{ret}, {_Property("id", config)} {(reverse ? "desc" : "asc")}"; return $"{ret}, {Property("id", config)} {(reverse ? "desc" : "asc")}";
} }
public static ( public static (
@ -89,7 +89,7 @@ public static class DapperHelper
string tableName = type.GetCustomAttribute<TableAttribute>()?.Name ?? $"{type.Name.ToSnakeCase()}s"; string tableName = type.GetCustomAttribute<TableAttribute>()?.Name ?? $"{type.Name.ToSnakeCase()}s";
types.Add(type); types.Add(type);
projection.AppendLine($", r{relation}.* -- {type.Name} as r{relation}"); projection.AppendLine($", r{relation}.* -- {type.Name} as r{relation}");
join.Append($"\nleft join {tableName} as r{relation} on r{relation}.id = {_Property(rid, config)}"); join.Append($"\nleft join {tableName} as r{relation} on r{relation}.id = {Property(rid, config)}");
break; break;
case Include.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;
@ -173,8 +173,8 @@ public static class DapperHelper
Filter<T>.Ge(var property, var value) => Format(property, $">= {P(value)}"), Filter<T>.Ge(var property, var value) => Format(property, $">= {P(value)}"),
Filter<T>.Lt(var property, var value) => Format(property, $"< {P(value)}"), Filter<T>.Lt(var property, var value) => Format(property, $"< {P(value)}"),
Filter<T>.Le(var property, var value) => Format(property, $"> {P(value)}"), Filter<T>.Le(var property, var value) => Format(property, $"> {P(value)}"),
Filter<T>.Has(var property, var value) => $"{P(value)} = any({_Property(property, config):raw})", Filter<T>.Has(var property, var value) => $"{P(value)} = any({Property(property, config):raw})",
Filter<T>.EqRandom(var seed, var id) => $"md5({seed} || coalesce({string.Join(", ", config.Select(x => $"{x.Key}.id")):raw})) = md5({seed} || {id.ToString()})", Filter<T>.CmpRandom(var op, var seed, var id) => $"md5({seed} || coalesce({string.Join(", ", config.Select(x => $"{x.Key}.id")):raw})) {op:raw} md5({seed} || {id.ToString()})",
Filter<T>.Lambda(var lambda) => throw new NotSupportedException(), Filter<T>.Lambda(var lambda) => throw new NotSupportedException(),
_ => throw new NotImplementedException(), _ => throw new NotImplementedException(),
}; };
@ -200,7 +200,7 @@ public static class DapperHelper
Include<T>? include, Include<T>? include,
Filter<T>? filter, Filter<T>? filter,
Sort<T>? sort, Sort<T>? sort,
Pagination limit) Pagination? limit)
where T : class, IResource, IQuery where T : class, IResource, IQuery
{ {
InterpolatedSql.Dapper.SqlBuilders.SqlBuilder query = new(db, command); InterpolatedSql.Dapper.SqlBuilders.SqlBuilder query = new(db, command);
@ -214,7 +214,7 @@ public static class DapperHelper
throw new ArgumentException("Missing '/* includes */' placeholder in top level sql select to support includes."); throw new ArgumentException("Missing '/* includes */' placeholder in top level sql select to support includes.");
// Handle pagination, orders and filter. // Handle pagination, orders and filter.
if (limit.AfterID != null) if (limit?.AfterID != null)
{ {
T reference = await get(limit.AfterID.Value); T reference = await get(limit.AfterID.Value);
Filter<T>? keysetFilter = RepositoryHelper.KeysetPaginate(sort, reference, !limit.Reverse); Filter<T>? keysetFilter = RepositoryHelper.KeysetPaginate(sort, reference, !limit.Reverse);
@ -223,8 +223,9 @@ public static class DapperHelper
if (filter != null) if (filter != null)
query += ProcessFilter(filter, config); query += ProcessFilter(filter, config);
if (sort != null) if (sort != null)
query += $"\norder by {ProcessSort(sort, limit.Reverse, config):raw}"; query += $"\norder by {ProcessSort(sort, limit?.Reverse ?? false, config):raw}";
query += $"\nlimit {limit.Limit}"; if (limit != null)
query += $"\nlimit {limit.Limit}";
// Build query and prepare to do the query/projections // Build query and prepare to do the query/projections
IDapperSqlCommand cmd = query.Build(); IDapperSqlCommand cmd = query.Build();
@ -281,7 +282,7 @@ public static class DapperHelper
ParametersDictionary.LoadFrom(cmd), ParametersDictionary.LoadFrom(cmd),
splitOn: string.Join(',', types.Select(x => x == typeof(Image) ? "source" : "id")) splitOn: string.Join(',', types.Select(x => x == typeof(Image) ? "source" : "id"))
); );
if (limit.Reverse) if (limit?.Reverse == true)
data = data.Reverse(); data = data.Reverse();
return data.ToList(); return data.ToList();
} }

View File

@ -19,11 +19,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.Common; using System.Data.Common;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Utils; using Kyoo.Abstractions.Models.Utils;
using static Kyoo.Core.Controllers.DapperHelper;
namespace Kyoo.Core.Controllers; namespace Kyoo.Core.Controllers;
@ -74,9 +76,20 @@ public abstract class DapperRepository<T> : IRepository<T>
} }
/// <inheritdoc /> /// <inheritdoc />
public Task<ICollection<T>> FromIds(IList<Guid> ids, Include<T>? include = null) public async Task<ICollection<T>> FromIds(IList<Guid> ids, Include<T>? include = null)
{ {
throw new NotImplementedException(); return (await Database.Query<T>(
Sql,
Config,
Mapper,
(id) => Get(id),
include,
Filter.Or(ids.Select(x => new Filter<T>.Eq("id", x)).ToArray()),
sort: null,
limit: null
))
.OrderBy(x => ids.IndexOf(x.Id))
.ToList();
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -119,14 +119,20 @@ namespace Kyoo.Core.Controllers
ParameterExpression x = Expression.Parameter(typeof(T), "x"); ParameterExpression x = Expression.Parameter(typeof(T), "x");
Expression EqRandomHandler(string seed, Guid refId) Expression CmpRandomHandler(string cmp, string seed, Guid refId)
{ {
MethodInfo concat = typeof(string).GetMethod(nameof(string.Concat), new[] { typeof(string), typeof(string) })!; MethodInfo concat = typeof(string).GetMethod(nameof(string.Concat), new[] { typeof(string), typeof(string) })!;
Expression id = Expression.Call(Expression.Property(x, "ID"), nameof(Guid.ToString), null); Expression id = Expression.Call(Expression.Property(x, "ID"), nameof(Guid.ToString), null);
Expression xrng = Expression.Call(concat, Expression.Constant(seed), id); Expression xrng = Expression.Call(concat, Expression.Constant(seed), id);
Expression left = Expression.Call(typeof(DatabaseContext), nameof(DatabaseContext.MD5), null, xrng); Expression left = Expression.Call(typeof(DatabaseContext), nameof(DatabaseContext.MD5), null, xrng);
Expression right = Expression.Call(typeof(DatabaseContext), nameof(DatabaseContext.MD5), null, Expression.Constant($"{seed}{refId}")); Expression right = Expression.Call(typeof(DatabaseContext), nameof(DatabaseContext.MD5), null, Expression.Constant($"{seed}{refId}"));
return Expression.Equal(left, right); return cmp switch
{
"=" => Expression.Equal(left, right),
"<" => Expression.GreaterThan(left, right),
">" => Expression.LessThan(left, right),
_ => throw new NotImplementedException()
};
} }
BinaryExpression StringCompatibleExpression( BinaryExpression StringCompatibleExpression(
@ -154,7 +160,7 @@ namespace Kyoo.Core.Controllers
Filter<T>.Lt(var property, var value) => StringCompatibleExpression(Expression.LessThan, Expression.Property(x, property), Expression.Constant(value)), Filter<T>.Lt(var property, var value) => StringCompatibleExpression(Expression.LessThan, Expression.Property(x, property), Expression.Constant(value)),
Filter<T>.Le(var property, var value) => StringCompatibleExpression(Expression.LessThanOrEqual, Expression.Property(x, property), Expression.Constant(value)), Filter<T>.Le(var property, var value) => StringCompatibleExpression(Expression.LessThanOrEqual, Expression.Property(x, property), Expression.Constant(value)),
Filter<T>.Has(var property, var value) => Expression.Call(typeof(Enumerable), "Contains", new[] { value.GetType() }, Expression.Property(x, property), Expression.Constant(value)), Filter<T>.Has(var property, var value) => Expression.Call(typeof(Enumerable), "Contains", new[] { value.GetType() }, Expression.Property(x, property), Expression.Constant(value)),
Filter<T>.EqRandom(var seed, var refId) => EqRandomHandler(seed, refId), Filter<T>.CmpRandom(var op, var seed, var refId) => CmpRandomHandler(op, seed, refId),
Filter<T>.Lambda(var lambda) => ExpressionArgumentReplacer.ReplaceParams(lambda.Body, lambda.Parameters, x), Filter<T>.Lambda(var lambda) => ExpressionArgumentReplacer.ReplaceParams(lambda.Body, lambda.Parameters, x),
_ => throw new NotImplementedException(), _ => throw new NotImplementedException(),
}; };

View File

@ -90,7 +90,7 @@ public class RepositoryHelper
{ {
Filter<T> pEquals = pSeed == null Filter<T> pEquals = pSeed == null
? new Filter<T>.Eq(pKey, reference.GetType().GetProperty(pKey)?.GetValue(reference)) ? new Filter<T>.Eq(pKey, reference.GetType().GetProperty(pKey)?.GetValue(reference))
: new Filter<T>.EqRandom(pSeed, reference.Id); : new Filter<T>.CmpRandom("=", pSeed, reference.Id);
equals = Filter.And(equals, pEquals); equals = Filter.And(equals, pEquals);
} }
@ -100,7 +100,7 @@ public class RepositoryHelper
: (prop, val) => new Filter<T>.Lt(prop, val); : (prop, val) => new Filter<T>.Lt(prop, val);
Filter<T> last = seed == null Filter<T> last = seed == null
? comparer(key, value!) ? comparer(key, value!)
: new Filter<T>.EqRandom(seed, reference.Id); : new Filter<T>.CmpRandom(greaterThan ? ">" : "<", seed, reference.Id);
if (key != "random") if (key != "random")
{ {

View File

@ -16,6 +16,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;