mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-01 04:34:50 -04:00
Allow random queries to be paginated
This commit is contained in:
parent
e8b929d4ca
commit
e13f9c6aa8
@ -90,9 +90,6 @@ namespace Kyoo.Abstractions.Models
|
|||||||
{
|
{
|
||||||
Items = items;
|
Items = items;
|
||||||
This = url + query.ToQueryString();
|
This = url + query.ToQueryString();
|
||||||
|
|
||||||
if (!(query.TryGetValue("sortBy", out string? sort) && sort.Contains("random")))
|
|
||||||
{
|
|
||||||
if (items.Count > 0 && query.ContainsKey("afterID"))
|
if (items.Count > 0 && query.ContainsKey("afterID"))
|
||||||
{
|
{
|
||||||
query["afterID"] = items.First().Id.ToString();
|
query["afterID"] = items.First().Id.ToString();
|
||||||
@ -105,8 +102,6 @@ namespace Kyoo.Abstractions.Models
|
|||||||
query["afterID"] = items.Last().Id.ToString();
|
query["afterID"] = items.Last().Id.ToString();
|
||||||
Next = url + query.ToQueryString();
|
Next = url + query.ToQueryString();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
query.Remove("afterID");
|
query.Remove("afterID");
|
||||||
First = url + query.ToQueryString();
|
First = url + query.ToQueryString();
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ namespace Kyoo.Abstractions.Controllers
|
|||||||
public record Conglomerate(params Sort<T>[] List) : Sort<T>;
|
public record Conglomerate(params Sort<T>[] List) : Sort<T>;
|
||||||
|
|
||||||
/// <summary>Sort randomly items</summary>
|
/// <summary>Sort randomly items</summary>
|
||||||
public record Random() : Sort<T>;
|
public record Random(int seed) : Sort<T>;
|
||||||
|
|
||||||
/// <summary>The default sort method for the given type.</summary>
|
/// <summary>The default sort method for the given type.</summary>
|
||||||
public record Default : Sort<T>;
|
public record Default : Sort<T>;
|
||||||
@ -73,10 +73,13 @@ namespace Kyoo.Abstractions.Controllers
|
|||||||
if (string.IsNullOrEmpty(sortBy) || sortBy == "default")
|
if (string.IsNullOrEmpty(sortBy) || sortBy == "default")
|
||||||
return new Default();
|
return new Default();
|
||||||
if (sortBy == "random")
|
if (sortBy == "random")
|
||||||
return new Random();
|
return new Random(new System.Random().Next(int.MinValue, int.MaxValue));
|
||||||
if (sortBy.Contains(','))
|
if (sortBy.Contains(','))
|
||||||
return new Conglomerate(sortBy.Split(',').Select(From).ToArray());
|
return new Conglomerate(sortBy.Split(',').Select(From).ToArray());
|
||||||
|
|
||||||
|
if (sortBy.StartsWith("random:"))
|
||||||
|
return new Random(int.Parse(sortBy["random:".Length..]));
|
||||||
|
|
||||||
string key = sortBy.Contains(':') ? sortBy[..sortBy.IndexOf(':')] : sortBy;
|
string key = sortBy.Contains(':') ? sortBy[..sortBy.IndexOf(':')] : sortBy;
|
||||||
string? order = sortBy.Contains(':') ? sortBy[(sortBy.IndexOf(':') + 1)..] : null;
|
string? order = sortBy.Contains(':') ? sortBy[(sortBy.IndexOf(':') + 1)..] : null;
|
||||||
bool desendant = order switch
|
bool desendant = order switch
|
||||||
|
@ -28,6 +28,7 @@ using Kyoo.Abstractions.Models;
|
|||||||
using Kyoo.Abstractions.Models.Attributes;
|
using Kyoo.Abstractions.Models.Attributes;
|
||||||
using Kyoo.Abstractions.Models.Exceptions;
|
using Kyoo.Abstractions.Models.Exceptions;
|
||||||
using Kyoo.Core.Api;
|
using Kyoo.Core.Api;
|
||||||
|
using Kyoo.Postgresql;
|
||||||
using Kyoo.Utils;
|
using Kyoo.Utils;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@ -109,8 +110,9 @@ namespace Kyoo.Core.Controllers
|
|||||||
return _Sort(query, DefaultSort, then);
|
return _Sort(query, DefaultSort, then);
|
||||||
case Sort<T>.By(var key, var desc):
|
case Sort<T>.By(var key, var desc):
|
||||||
return _SortBy(query, x => EF.Property<T>(x, key), desc, then);
|
return _SortBy(query, x => EF.Property<T>(x, key), desc, then);
|
||||||
case Sort<T>.Random:
|
case Sort<T>.Random(var seed):
|
||||||
return _SortBy(query, x => EF.Functions.Random(), false, then);
|
// NOTE: To edit this, don't forget to edit the random handiling inside the KeysetPaginate function
|
||||||
|
return _SortBy(query, x => DatabaseContext.MD5(seed + x.Id.ToString()), false, then);
|
||||||
case Sort<T>.Conglomerate(var sorts):
|
case Sort<T>.Conglomerate(var sorts):
|
||||||
IOrderedQueryable<T> nQuery = _Sort(query, sorts.First(), false);
|
IOrderedQueryable<T> nQuery = _Sort(query, sorts.First(), false);
|
||||||
foreach (Sort<T> sort in sorts.Skip(1))
|
foreach (Sort<T> sort in sorts.Skip(1))
|
||||||
@ -136,6 +138,8 @@ namespace Kyoo.Core.Controllers
|
|||||||
: (greaterThan ? Expression.GreaterThan : Expression.LessThan);
|
: (greaterThan ? Expression.GreaterThan : Expression.LessThan);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record SortIndicator(string key, bool desc, string? seed);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a filter (where) expression on the query to skip everything before/after the referenceID.
|
/// Create a filter (where) expression on the query to skip everything before/after the referenceID.
|
||||||
/// The generalized expression for this in pseudocode is:
|
/// The generalized expression for this in pseudocode is:
|
||||||
@ -164,45 +168,66 @@ namespace Kyoo.Core.Controllers
|
|||||||
ParameterExpression x = Expression.Parameter(typeof(T), "x");
|
ParameterExpression x = Expression.Parameter(typeof(T), "x");
|
||||||
ConstantExpression referenceC = Expression.Constant(reference, typeof(T));
|
ConstantExpression referenceC = Expression.Constant(reference, typeof(T));
|
||||||
|
|
||||||
IEnumerable<Sort<T>.By> GetSortsBy(Sort<T> sort)
|
void GetRandomSortKeys(string seed, out Expression left, out Expression right)
|
||||||
|
{
|
||||||
|
MethodInfo concat = typeof(string).GetMethod(nameof(string.Concat), new[] { typeof(string), typeof(string) });
|
||||||
|
Expression id = Expression.Call(Expression.Property(x, "ID"), nameof(int.ToString), null);
|
||||||
|
Expression xrng = Expression.Call(concat, Expression.Constant(seed), id);
|
||||||
|
right = Expression.Call(typeof(DatabaseContext), nameof(DatabaseContext.MD5), null, Expression.Constant($"{seed}{reference.Id}"));
|
||||||
|
left = Expression.Call(typeof(DatabaseContext), nameof(DatabaseContext.MD5), null, xrng);
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerable<SortIndicator> GetSortsBy(Sort<T> sort)
|
||||||
{
|
{
|
||||||
return sort switch
|
return sort switch
|
||||||
{
|
{
|
||||||
Sort<T>.Default => GetSortsBy(DefaultSort),
|
Sort<T>.Default => GetSortsBy(DefaultSort),
|
||||||
Sort<T>.By @sortBy => new[] { sortBy },
|
Sort<T>.By @sortBy => new[] { new SortIndicator(sortBy.Key, sortBy.Desendant, null) },
|
||||||
Sort<T>.Conglomerate(var list) => list.SelectMany(GetSortsBy),
|
Sort<T>.Conglomerate(var list) => list.SelectMany(GetSortsBy),
|
||||||
Sort<T>.Random => throw new ArgumentException("Impossible to paginate randomly sorted items."),
|
Sort<T>.Random(var seed) => new[] { new SortIndicator("random", false, seed.ToString()) },
|
||||||
_ => Array.Empty<Sort<T>.By>(),
|
_ => Array.Empty<SortIndicator>(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't forget that every sorts must end with a ID sort (to differentiate equalities).
|
// Don't forget that every sorts must end with a ID sort (to differentiate equalities).
|
||||||
Sort<T>.By id = new(x => x.Id);
|
IEnumerable<SortIndicator> sorts = GetSortsBy(sort)
|
||||||
IEnumerable<Sort<T>.By> sorts = GetSortsBy(sort).Append(id);
|
.Append(new SortIndicator("Id", false, null));
|
||||||
|
|
||||||
BinaryExpression filter = null;
|
BinaryExpression filter = null;
|
||||||
List<Sort<T>.By> previousSteps = new();
|
List<SortIndicator> previousSteps = new();
|
||||||
// TODO: Add an outer query >= for perf
|
// TODO: Add an outer query >= for perf
|
||||||
// PERF: See https://use-the-index-luke.com/sql/partial-results/fetch-next-page#sb-equivalent-logic
|
// PERF: See https://use-the-index-luke.com/sql/partial-results/fetch-next-page#sb-equivalent-logic
|
||||||
foreach ((string key, bool desc) in sorts)
|
foreach ((string key, bool desc, string? seed) in sorts)
|
||||||
{
|
{
|
||||||
BinaryExpression compare = null;
|
BinaryExpression compare = null;
|
||||||
PropertyInfo property = typeof(T).GetProperty(key);
|
PropertyInfo property = key != "random"
|
||||||
|
? typeof(T).GetProperty(key)
|
||||||
|
: null;
|
||||||
|
|
||||||
// Comparing a value with null always return false so we short opt < > comparisons with null.
|
// Comparing a value with null always return false so we short opt < > comparisons with null.
|
||||||
if (property!.GetValue(reference) == null)
|
if (property != null && property.GetValue(reference) == null)
|
||||||
{
|
{
|
||||||
previousSteps.Add(new Sort<T>.By(key, desc));
|
previousSteps.Add(new SortIndicator(key, desc, seed));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create all the equality statements for previous sorts.
|
// Create all the equality statements for previous sorts.
|
||||||
foreach ((string pKey, bool pDesc) in previousSteps)
|
foreach ((string pKey, bool pDesc, string? pSeed) in previousSteps)
|
||||||
{
|
{
|
||||||
BinaryExpression pcompare = Expression.Equal(
|
BinaryExpression pcompare;
|
||||||
|
|
||||||
|
if (pSeed == null)
|
||||||
|
{
|
||||||
|
pcompare = Expression.Equal(
|
||||||
Expression.Property(x, pKey),
|
Expression.Property(x, pKey),
|
||||||
Expression.Property(referenceC, pKey)
|
Expression.Property(referenceC, pKey)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GetRandomSortKeys(pSeed, out Expression left, out Expression right);
|
||||||
|
pcompare = Expression.Equal(left, right);
|
||||||
|
}
|
||||||
compare = compare != null
|
compare = compare != null
|
||||||
? Expression.AndAlso(compare, pcompare)
|
? Expression.AndAlso(compare, pcompare)
|
||||||
: pcompare;
|
: pcompare;
|
||||||
@ -210,14 +235,21 @@ namespace Kyoo.Core.Controllers
|
|||||||
|
|
||||||
// Create the last comparison of the statement.
|
// Create the last comparison of the statement.
|
||||||
Func<Expression, Expression, BinaryExpression> comparer = _GetComparisonExpression(desc, next, false);
|
Func<Expression, Expression, BinaryExpression> comparer = _GetComparisonExpression(desc, next, false);
|
||||||
MemberExpression xkey = Expression.Property(x, key);
|
Expression xkey;
|
||||||
MemberExpression rkey = Expression.Property(referenceC, key);
|
Expression rkey;
|
||||||
|
if (seed == null)
|
||||||
|
{
|
||||||
|
xkey = Expression.Property(x, key);
|
||||||
|
rkey = Expression.Property(referenceC, key);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
GetRandomSortKeys(seed, out xkey, out rkey);
|
||||||
BinaryExpression lastCompare = ApiHelper.StringCompatibleExpression(comparer, xkey, rkey);
|
BinaryExpression lastCompare = ApiHelper.StringCompatibleExpression(comparer, xkey, rkey);
|
||||||
|
|
||||||
// Comparing a value with null always return false for nulls so we must add nulls to the results manually.
|
// Comparing a value with null always return false for nulls so we must add nulls to the results manually.
|
||||||
// Postgres sorts them after values so we will do the same
|
// Postgres sorts them after values so we will do the same
|
||||||
// We only add this condition if the column type is nullable
|
// We only add this condition if the column type is nullable
|
||||||
if (Nullable.GetUnderlyingType(property.PropertyType) != null)
|
if (property != null && Nullable.GetUnderlyingType(property.PropertyType) != null)
|
||||||
{
|
{
|
||||||
BinaryExpression equalNull = Expression.Equal(xkey, Expression.Constant(null));
|
BinaryExpression equalNull = Expression.Equal(xkey, Expression.Constant(null));
|
||||||
lastCompare = Expression.OrElse(lastCompare, equalNull);
|
lastCompare = Expression.OrElse(lastCompare, equalNull);
|
||||||
@ -231,7 +263,7 @@ namespace Kyoo.Core.Controllers
|
|||||||
? Expression.OrElse(filter, compare)
|
? Expression.OrElse(filter, compare)
|
||||||
: compare;
|
: compare;
|
||||||
|
|
||||||
previousSteps.Add(new(key, desc));
|
previousSteps.Add(new SortIndicator(key, desc, seed));
|
||||||
}
|
}
|
||||||
return Expression.Lambda<Func<T, bool>>(filter!, x);
|
return Expression.Lambda<Func<T, bool>>(filter!, x);
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,6 @@ using System.Linq.Expressions;
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Kyoo.Abstractions.Models;
|
using Kyoo.Abstractions.Models;
|
||||||
using Kyoo.Utils;
|
|
||||||
|
|
||||||
namespace Kyoo.Core.Api
|
namespace Kyoo.Core.Api
|
||||||
{
|
{
|
||||||
@ -53,7 +52,7 @@ namespace Kyoo.Core.Api
|
|||||||
[NotNull] Expression left,
|
[NotNull] Expression left,
|
||||||
[NotNull] Expression right)
|
[NotNull] Expression right)
|
||||||
{
|
{
|
||||||
if (left is not MemberExpression member || ((PropertyInfo)member.Member).PropertyType != typeof(string))
|
if (left.Type != typeof(string))
|
||||||
return operand(left, right);
|
return operand(left, right);
|
||||||
MethodCallExpression call = Expression.Call(typeof(string), "Compare", null, left, right);
|
MethodCallExpression call = Expression.Call(typeof(string), "Compare", null, left, right);
|
||||||
return operand(call, Expression.Constant(0));
|
return operand(call, Expression.Constant(0));
|
||||||
|
@ -41,6 +41,13 @@ namespace Kyoo.Postgresql
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public abstract class DatabaseContext : DbContext
|
public abstract class DatabaseContext : DbContext
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Calculate the MD5 of a string, can only be used in database context.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="str">The string to hash</param>
|
||||||
|
/// <returns>The hash</returns>
|
||||||
|
public static string MD5(string str) => throw new NotSupportedException();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All collections of Kyoo. See <see cref="Collection"/>.
|
/// All collections of Kyoo. See <see cref="Collection"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>Kyoo.Postgresql</AssemblyName>
|
<AssemblyName>Kyoo.Postgresql</AssemblyName>
|
||||||
<RootNamespace>Kyoo.Postgresql</RootNamespace>
|
<RootNamespace>Kyoo.Postgresql</RootNamespace>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -18,12 +18,14 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using EFCore.NamingConventions.Internal;
|
using EFCore.NamingConventions.Internal;
|
||||||
using Kyoo.Abstractions.Models;
|
using Kyoo.Abstractions.Models;
|
||||||
using Kyoo.Utils;
|
using Kyoo.Utils;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
|
|
||||||
namespace Kyoo.Postgresql
|
namespace Kyoo.Postgresql
|
||||||
@ -104,6 +106,18 @@ namespace Kyoo.Postgresql
|
|||||||
modelBuilder.HasPostgresEnum<Genre>();
|
modelBuilder.HasPostgresEnum<Genre>();
|
||||||
modelBuilder.HasPostgresEnum<ItemKind>();
|
modelBuilder.HasPostgresEnum<ItemKind>();
|
||||||
|
|
||||||
|
modelBuilder.HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(MD5)))
|
||||||
|
.HasTranslation(args =>
|
||||||
|
new SqlFunctionExpression(
|
||||||
|
"md5",
|
||||||
|
args,
|
||||||
|
nullable: true,
|
||||||
|
argumentsPropagateNullability: new[] { false },
|
||||||
|
type: args[0].Type,
|
||||||
|
typeMapping: args[0].TypeMapping
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user