[backend/core] Refactor QueryableFtsExtensions

This commit is contained in:
Laura Hausmann 2024-06-17 20:37:48 +02:00
parent 1d02bd7119
commit 752161d7db
No known key found for this signature in database
GPG key ID: D044E84C5BE01605

View file

@ -43,6 +43,8 @@ public static class QueryableFtsExtensions
}); });
} }
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global",
Justification = "Projectable chain must have consistent visibility")]
internal static (string username, string? host) UserToTuple(string filter, Config.InstanceSection config) internal static (string username, string? host) UserToTuple(string filter, Config.InstanceSection config)
{ {
filter = filter.TrimStart('@'); filter = filter.TrimStart('@');
@ -55,24 +57,22 @@ public static class QueryableFtsExtensions
/// <returns> /// <returns>
/// The input variable host, or null if it matches the configured web or account domain. /// The input variable host, or null if it matches the configured web or account domain.
/// </returns> /// </returns>
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global",
Justification = "Projectable chain must have consistent visibility")]
internal static string? LocalDomainCheck(string? host, Config.InstanceSection config) => internal static string? LocalDomainCheck(string? host, Config.InstanceSection config) =>
host == null || host == config.WebDomain || host == config.AccountDomain ? null : host; host == null || host == config.WebDomain || host == config.AccountDomain ? null : host;
[Projectable]
private static IQueryable<Note> ApplyAfterFilter(this IQueryable<Note> query, AfterFilter filter) private static IQueryable<Note> ApplyAfterFilter(this IQueryable<Note> query, AfterFilter filter)
=> query.Where(p => p.CreatedAt >= filter.Value.ToDateTime(TimeOnly.MinValue).ToUniversalTime()); => query.Where(p => p.CreatedAt >= filter.Value.ToDateTime(TimeOnly.MinValue).ToUniversalTime());
[Projectable]
private static IQueryable<Note> ApplyBeforeFilter(this IQueryable<Note> query, BeforeFilter filter) private static IQueryable<Note> ApplyBeforeFilter(this IQueryable<Note> query, BeforeFilter filter)
=> query.Where(p => p.CreatedAt < filter.Value.ToDateTime(TimeOnly.MinValue).ToUniversalTime()); => query.Where(p => p.CreatedAt < filter.Value.ToDateTime(TimeOnly.MinValue).ToUniversalTime());
[Projectable]
private static IQueryable<Note> ApplyWordFilter( private static IQueryable<Note> ApplyWordFilter(
this IQueryable<Note> query, WordFilter filter, CaseFilterType caseSensitivity, MatchFilterType matchType this IQueryable<Note> query, WordFilter filter, CaseFilterType caseSensitivity, MatchFilterType matchType
) => query.Where(p => p.FtsQueryPreEscaped(PreEscapeFtsQuery(filter.Value, matchType), filter.Negated, ) => query.Where(p => p.FtsQueryPreEscaped(PreEscapeFtsQuery(filter.Value, matchType), filter.Negated,
caseSensitivity, matchType)); caseSensitivity, matchType));
[Projectable]
private static IQueryable<Note> ApplyMultiWordFilter( private static IQueryable<Note> ApplyMultiWordFilter(
this IQueryable<Note> query, MultiWordFilter filter, CaseFilterType caseSensitivity, MatchFilterType matchType this IQueryable<Note> query, MultiWordFilter filter, CaseFilterType caseSensitivity, MatchFilterType matchType
) => query.Where(p => p.FtsQueryOneOf(filter.Values, caseSensitivity, matchType)); ) => query.Where(p => p.FtsQueryOneOf(filter.Values, caseSensitivity, matchType));
@ -88,170 +88,157 @@ public static class QueryableFtsExtensions
return query.Where(expr); return query.Where(expr);
} }
[Projectable]
private static IQueryable<Note> ApplyInstanceFilter( private static IQueryable<Note> ApplyInstanceFilter(
this IQueryable<Note> query, InstanceFilter filter, Config.InstanceSection config this IQueryable<Note> query, InstanceFilter filter, Config.InstanceSection config
) => query.Where(p => filter.Negated ) => query.Where(p => filter.Negated
? p.UserHost != LocalDomainCheck(filter.Value, config) ? p.UserHost != LocalDomainCheck(filter.Value, config)
: p.UserHost == LocalDomainCheck(filter.Value, config)); : p.UserHost == LocalDomainCheck(filter.Value, config));
[Projectable]
private static IQueryable<Note> ApplyMentionFilter( private static IQueryable<Note> ApplyMentionFilter(
this IQueryable<Note> query, MentionFilter filter, Config.InstanceSection config, DatabaseContext db this IQueryable<Note> query, MentionFilter filter, Config.InstanceSection config, DatabaseContext db
) => query.Where(p => p.Mentions.UserSubqueryContains(filter.Value, filter.Negated, config, db)); ) => query.Where(p => p.Mentions.UserSubqueryContains(filter.Value, filter.Negated, config, db));
[Projectable]
private static IQueryable<Note> ApplyReplyFilter( private static IQueryable<Note> ApplyReplyFilter(
this IQueryable<Note> query, ReplyFilter filter, Config.InstanceSection config, DatabaseContext db this IQueryable<Note> query, ReplyFilter filter, Config.InstanceSection config, DatabaseContext db
) => query.Where(p => p.Reply != null && ) => query.Where(p => p.Reply != null &&
p.Reply.User.UserSubqueryMatches(filter.Value, filter.Negated, config, db)); p.Reply.User.UserSubqueryMatches(filter.Value, filter.Negated, config, db));
[Projectable]
private static IQueryable<Note> ApplyInFilter( private static IQueryable<Note> ApplyInFilter(
this IQueryable<Note> query, InFilter filter, User user, DatabaseContext db this IQueryable<Note> query, InFilter filter, User user, DatabaseContext db
) => filter.Value.Equals(InFilterType.Bookmarks) )
? query.ApplyInBookmarksFilter(user, filter.Negated, db) {
: filter.Value.Equals(InFilterType.Likes) return filter.Value switch
? query.ApplyInLikesFilter(user, filter.Negated, db) {
: filter.Value.Equals(InFilterType.Reactions) { IsLikes: true } => query.ApplyInLikesFilter(user, filter.Negated, db),
? query.ApplyInReactionsFilter(user, filter.Negated, db) { IsBookmarks: true } => query.ApplyInBookmarksFilter(user, filter.Negated, db),
: query.ApplyInInteractionsFilter(user, filter.Negated, db); { IsReactions: true } => query.ApplyInReactionsFilter(user, filter.Negated, db),
{ IsInteractions: true } => query.ApplyInInteractionsFilter(user, filter.Negated, db),
_ => throw new ArgumentOutOfRangeException(nameof(filter), filter.Value, null)
};
}
[Projectable]
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")] [SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")]
internal static IQueryable<Note> ApplyInBookmarksFilter( private static IQueryable<Note> ApplyInBookmarksFilter(
this IQueryable<Note> query, User user, bool negated, DatabaseContext db this IQueryable<Note> query, User user, bool negated, DatabaseContext db
) => query.Where(p => negated ) => query.Where(p => negated
? !db.Users.First(u => u == user).HasBookmarked(p) ? !db.Users.First(u => u == user).HasBookmarked(p)
: db.Users.First(u => u == user).HasBookmarked(p)); : db.Users.First(u => u == user).HasBookmarked(p));
[Projectable]
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")] [SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")]
internal static IQueryable<Note> ApplyInLikesFilter( private static IQueryable<Note> ApplyInLikesFilter(
this IQueryable<Note> query, User user, bool negated, DatabaseContext db this IQueryable<Note> query, User user, bool negated, DatabaseContext db
) => query.Where(p => negated ) => query.Where(p => negated
? !db.Users.First(u => u == user).HasLiked(p) ? !db.Users.First(u => u == user).HasLiked(p)
: db.Users.First(u => u == user).HasLiked(p)); : db.Users.First(u => u == user).HasLiked(p));
[Projectable]
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")] [SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")]
internal static IQueryable<Note> ApplyInReactionsFilter( private static IQueryable<Note> ApplyInReactionsFilter(
this IQueryable<Note> query, User user, bool negated, DatabaseContext db this IQueryable<Note> query, User user, bool negated, DatabaseContext db
) => query.Where(p => negated ) => query.Where(p => negated
? !db.Users.First(u => u == user).HasReacted(p) ? !db.Users.First(u => u == user).HasReacted(p)
: db.Users.First(u => u == user).HasReacted(p)); : db.Users.First(u => u == user).HasReacted(p));
[Projectable]
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")] [SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")]
internal static IQueryable<Note> ApplyInInteractionsFilter( private static IQueryable<Note> ApplyInInteractionsFilter(
this IQueryable<Note> query, User user, bool negated, DatabaseContext db this IQueryable<Note> query, User user, bool negated, DatabaseContext db
) => query.Where(p => negated ) => query.Where(p => negated
? !db.Users.First(u => u == user).HasInteractedWith(p) ? !db.Users.First(u => u == user).HasInteractedWith(p)
: db.Users.First(u => u == user).HasInteractedWith(p)); : db.Users.First(u => u == user).HasInteractedWith(p));
[Projectable] private static IQueryable<Note> ApplyMiscFilter(this IQueryable<Note> query, MiscFilter filter, User user)
private static IQueryable<Note> ApplyMiscFilter( {
this IQueryable<Note> query, MiscFilter filter, User user return filter.Value switch
) => filter.Value.Equals(MiscFilterType.Followers) {
? query.ApplyFollowersFilter(user, filter.Negated) { IsFollowers: true } => query.ApplyFollowersFilter(user, filter.Negated),
: filter.Value.Equals(MiscFilterType.Following) { IsFollowing: true } => query.ApplyFollowingFilter(user, filter.Negated),
? query.ApplyFollowingFilter(user, filter.Negated) { IsRenotes: true } => query.ApplyRepliesFilter(filter.Negated),
: filter.Value.Equals(MiscFilterType.Replies) { IsReplies: true } => query.ApplyBoostsFilter(filter.Negated),
? query.ApplyRepliesFilter(filter.Negated) _ => throw new ArgumentOutOfRangeException(nameof(filter))
: query.ApplyBoostsFilter(filter.Negated); };
}
[Projectable]
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")] [SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")]
internal static IQueryable<Note> ApplyFollowersFilter( private static IQueryable<Note> ApplyFollowersFilter(this IQueryable<Note> query, User user, bool negated)
this IQueryable<Note> query, User user, bool negated => query.Where(p => negated ? !p.User.IsFollowing(user) : p.User.IsFollowing(user));
) => query.Where(p => negated
? !p.User.IsFollowing(user)
: p.User.IsFollowing(user));
[Projectable]
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")] [SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")]
internal static IQueryable<Note> ApplyFollowingFilter( private static IQueryable<Note> ApplyFollowingFilter(this IQueryable<Note> query, User user, bool negated)
this IQueryable<Note> query, User user, bool negated => query.Where(p => negated ? !p.User.IsFollowedBy(user) : p.User.IsFollowedBy(user));
) => query.Where(p => negated
? !p.User.IsFollowedBy(user)
: p.User.IsFollowedBy(user));
[Projectable] private static IQueryable<Note> ApplyRepliesFilter(this IQueryable<Note> query, bool negated)
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")] => query.Where(p => negated ? p.Reply == null : p.Reply != null);
internal static IQueryable<Note> ApplyRepliesFilter(
this IQueryable<Note> query, bool negated
) => query.Where(p => negated
? p.Reply == null
: p.Reply != null);
[Projectable] private static IQueryable<Note> ApplyBoostsFilter(this IQueryable<Note> query, bool negated)
[SuppressMessage("ReSharper", "EntityFramework.UnsupportedServerSideFunctionCall", Justification = "Projectables")] => query.Where(p => negated ? !p.IsPureRenote : p.IsPureRenote);
internal static IQueryable<Note> ApplyBoostsFilter(
this IQueryable<Note> query, bool negated
) => query.Where(p => negated
? !p.IsPureRenote
: p.IsPureRenote);
[Projectable]
private static IQueryable<Note> ApplyAttachmentFilter(this IQueryable<Note> query, AttachmentFilter filter) private static IQueryable<Note> ApplyAttachmentFilter(this IQueryable<Note> query, AttachmentFilter filter)
=> filter.Negated ? query.ApplyNegatedAttachmentFilter(filter) : query.ApplyRegularAttachmentFilter(filter); => filter.Negated ? query.ApplyNegatedAttachmentFilter(filter) : query.ApplyRegularAttachmentFilter(filter);
[Projectable] private static IQueryable<Note> ApplyRegularAttachmentFilter(this IQueryable<Note> query, AttachmentFilter filter)
internal static IQueryable<Note> ApplyRegularAttachmentFilter(this IQueryable<Note> query, AttachmentFilter filter)
=> AttachmentQuery(filter.Value)
? filter.Value.Equals(AttachmentFilterType.Poll)
? query.Where(p => p.HasPoll)
: query.Where(p => EF.Functions.ILike(p.RawAttachments, GetAttachmentILikeQuery(filter.Value)))
: filter.Value.Equals(AttachmentFilterType.Any)
? query.Where(p => p.AttachedFileTypes.Count != 0)
: query.Where(p => p.AttachedFileTypes.Count != 0 &&
(!EF.Functions.ILike(p.RawAttachments,
GetAttachmentILikeQuery(AttachmentFilterType.Image)) ||
!EF.Functions.ILike(p.RawAttachments,
GetAttachmentILikeQuery(AttachmentFilterType.Video)) ||
!EF.Functions.ILike(p.RawAttachments,
GetAttachmentILikeQuery(AttachmentFilterType.Audio))));
[Projectable]
internal static IQueryable<Note> ApplyNegatedAttachmentFilter(this IQueryable<Note> query, AttachmentFilter filter)
=> AttachmentQuery(filter.Value)
? filter.Value.Equals(AttachmentFilterType.Poll)
? query.Where(p => !p.HasPoll)
: query.Where(p => !EF.Functions.ILike(p.RawAttachments, GetAttachmentILikeQuery(filter.Value)))
: filter.Value.Equals(AttachmentFilterType.Any)
? query.Where(p => p.AttachedFileTypes.Count == 0)
: query.Where(p => EF.Functions
.ILike(p.RawAttachments, GetAttachmentILikeQuery(AttachmentFilterType.Image)) ||
EF.Functions
.ILike(p.RawAttachments, GetAttachmentILikeQuery(AttachmentFilterType.Video)) ||
EF.Functions
.ILike(p.RawAttachments, GetAttachmentILikeQuery(AttachmentFilterType.Audio)));
internal static bool AttachmentQuery(AttachmentFilterType filter)
{ {
if (filter.Equals(AttachmentFilterType.Image)) if (filter.Value.IsAny)
return true; return query.Where(p => p.AttachedFileTypes.Count != 0);
if (filter.Equals(AttachmentFilterType.Video)) if (filter.Value.IsPoll)
return true; return query.Where(p => p.HasPoll);
if (filter.Equals(AttachmentFilterType.Audio))
return true; if (filter.Value.IsImage || filter.Value.IsVideo || filter.Value.IsAudio)
if (filter.Equals(AttachmentFilterType.Poll)) {
return true; return query.Where(p => p.AttachedFileTypes.Count != 0 &&
return false; EF.Functions.ILike(p.RawAttachments, GetAttachmentILikeQuery(filter.Value)));
}
if (filter.Value.IsFile)
{
return query.Where(p => p.AttachedFileTypes.Count != 0 &&
(!EF.Functions.ILike(p.RawAttachments,
GetAttachmentILikeQuery(AttachmentFilterType.Image)) ||
!EF.Functions.ILike(p.RawAttachments,
GetAttachmentILikeQuery(AttachmentFilterType.Video)) ||
!EF.Functions.ILike(p.RawAttachments,
GetAttachmentILikeQuery(AttachmentFilterType.Audio))));
}
throw new ArgumentOutOfRangeException(nameof(filter), filter.Value, null);
} }
private static IQueryable<Note> ApplyNegatedAttachmentFilter(this IQueryable<Note> query, AttachmentFilter filter)
{
if (filter.Value.IsAny)
return query.Where(p => p.AttachedFileTypes.Count == 0);
if (filter.Value.IsPoll)
return query.Where(p => !p.HasPoll);
if (filter.Value.IsImage || filter.Value.IsVideo || filter.Value.IsAudio)
return query.Where(p => !EF.Functions.ILike(p.RawAttachments, GetAttachmentILikeQuery(filter.Value)));
if (filter.Value.IsFile)
{
return query.Where(p => EF.Functions
.ILike(p.RawAttachments, GetAttachmentILikeQuery(AttachmentFilterType.Image)) ||
EF.Functions
.ILike(p.RawAttachments, GetAttachmentILikeQuery(AttachmentFilterType.Video)) ||
EF.Functions
.ILike(p.RawAttachments, GetAttachmentILikeQuery(AttachmentFilterType.Audio)));
}
throw new ArgumentOutOfRangeException(nameof(filter), filter.Value, null);
}
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global",
Justification = "Projectable chain must have consistent visibility")]
internal static string GetAttachmentILikeQuery(AttachmentFilterType filter) internal static string GetAttachmentILikeQuery(AttachmentFilterType filter)
{ {
if (filter.Equals(AttachmentFilterType.Image)) return filter switch
return "%image/%"; {
if (filter.Equals(AttachmentFilterType.Video)) { IsImage: true } => "%image/%",
return "%video/%"; { IsVideo: true } => "%video/%",
if (filter.Equals(AttachmentFilterType.Audio)) { IsAudio: true } => "%audio/%",
return "%audio/%"; _ => throw new ArgumentOutOfRangeException(nameof(filter), filter, null)
throw new ArgumentOutOfRangeException(nameof(filter)); };
} }
[Projectable] [Projectable]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global",
Justification = "Projectable chain must have consistent visibility")]
internal static bool UserSubqueryMatches( internal static bool UserSubqueryMatches(
this User user, string filter, bool negated, Config.InstanceSection config, DatabaseContext db this User user, string filter, bool negated, Config.InstanceSection config, DatabaseContext db
) => negated ) => negated
@ -259,6 +246,8 @@ public static class QueryableFtsExtensions
: UserSubquery(UserToTuple(filter, config), db).Contains(user); : UserSubquery(UserToTuple(filter, config), db).Contains(user);
[Projectable] [Projectable]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global",
Justification = "Projectable chain must have consistent visibility")]
internal static bool UserSubqueryContains( internal static bool UserSubqueryContains(
this IEnumerable<string> userIds, string filter, bool negated, Config.InstanceSection config, DatabaseContext db this IEnumerable<string> userIds, string filter, bool negated, Config.InstanceSection config, DatabaseContext db
) => negated ) => negated
@ -266,10 +255,14 @@ public static class QueryableFtsExtensions
: userIds.Any(p => p == UserSubquery(UserToTuple(filter, config), db).Select(i => i.Id).FirstOrDefault()); : userIds.Any(p => p == UserSubquery(UserToTuple(filter, config), db).Select(i => i.Id).FirstOrDefault());
[Projectable] [Projectable]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global",
Justification = "Projectable chain must have consistent visibility")]
internal static IQueryable<User> UserSubquery((string username, string? host) filter, DatabaseContext db) => internal static IQueryable<User> UserSubquery((string username, string? host) filter, DatabaseContext db) =>
db.Users.Where(p => p.UsernameLower == filter.username && p.Host == filter.host); db.Users.Where(p => p.UsernameLower == filter.username && p.Host == filter.host);
[Projectable] [Projectable]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global",
Justification = "Projectable chain must have consistent visibility")]
internal static bool FtsQueryPreEscaped( internal static bool FtsQueryPreEscaped(
this Note note, string query, bool negated, CaseFilterType caseSensitivity, MatchFilterType matchType this Note note, string query, bool negated, CaseFilterType caseSensitivity, MatchFilterType matchType
) => matchType.Equals(MatchFilterType.Substring) ) => matchType.Equals(MatchFilterType.Substring)
@ -302,6 +295,8 @@ public static class QueryableFtsExtensions
: EfHelpers.EscapeRegexQuery(query); : EfHelpers.EscapeRegexQuery(query);
[Projectable] [Projectable]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global",
Justification = "Projectable chain must have consistent visibility")]
internal static bool FtsQueryOneOf( internal static bool FtsQueryOneOf(
this Note note, IEnumerable<string> words, CaseFilterType caseSensitivity, MatchFilterType matchType this Note note, IEnumerable<string> words, CaseFilterType caseSensitivity, MatchFilterType matchType
) => words.Select(p => PreEscapeFtsQuery(p, matchType)) ) => words.Select(p => PreEscapeFtsQuery(p, matchType))