perf(ui): push task-list filtering into SQL
LoadForListAsync loaded the entire tasks table (all lists, two Include joins) then filtered to the selected list in C#, never using idx_tasks_list_id/idx_tasks_status. ITaskListFilter now exposes MatchExpression, an Expression<Func<TaskEntity,bool>> that EF Core can translate to SQL; a new TaskListFilterBase compiles it on demand for the existing in-memory Matches(). The primary match query now runs as db.Tasks.Where(filter.MatchExpression), with two small follow-up queries (scoped by parent-id IN-lists) for contextual planning-parent rows and pulled-in children, followed by an explicit re-sort to restore the global SortOrder/CreatedAt ordering Regroup() depends on.
This commit is contained in:
@@ -336,26 +336,53 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
||||
try
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync(ct);
|
||||
var all = await db.Tasks
|
||||
.Include(t => t.List)
|
||||
.Include(t => t.Worktree)
|
||||
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
var filter = _filters.Resolve(list.Id);
|
||||
var baseQuery = db.Tasks.Include(t => t.List).Include(t => t.Worktree);
|
||||
|
||||
var filteredList = filter is null
|
||||
? new List<TaskEntity>()
|
||||
: await baseQuery.Where(filter.MatchExpression).ToListAsync(ct);
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var filter = _filters.Resolve(list.Id);
|
||||
var filteredList = filter is null
|
||||
? new List<TaskEntity>()
|
||||
: all.Where(t => filter.Matches(t) || filter.MatchesAsContext(t, all)).ToList();
|
||||
var topIds = filteredList.Where(t => t.ParentTaskId == null).Select(t => t.Id).ToHashSet();
|
||||
var existingIds = filteredList.Select(t => t.Id).ToHashSet();
|
||||
foreach (var c in all.Where(t => t.ParentTaskId != null && topIds.Contains(t.ParentTaskId!)))
|
||||
if (filter is not null)
|
||||
{
|
||||
if (existingIds.Add(c.Id))
|
||||
filteredList.Add(c);
|
||||
// Contextual parent rows (e.g. a planning parent hosting an already-matched queued/running
|
||||
// child): only fetch candidates that could plausibly qualify — parents of tasks we already
|
||||
// matched — instead of scanning the whole table.
|
||||
var candidateParentIds = filteredList
|
||||
.Where(t => t.ParentTaskId != null)
|
||||
.Select(t => t.ParentTaskId!)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
if (candidateParentIds.Count > 0)
|
||||
{
|
||||
var existingParentIds = filteredList.Select(t => t.Id).ToHashSet();
|
||||
var parentCandidates = await baseQuery
|
||||
.Where(t => candidateParentIds.Contains(t.Id) && !existingParentIds.Contains(t.Id))
|
||||
.ToListAsync(ct);
|
||||
foreach (var p in parentCandidates)
|
||||
if (filter.MatchesAsContext(p, filteredList))
|
||||
filteredList.Add(p);
|
||||
}
|
||||
}
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
// Pull in every child of an already-matched top-level row, regardless of whether the child
|
||||
// itself matches the filter, so subtasks render nested under their parent.
|
||||
var topIds = filteredList.Where(t => t.ParentTaskId == null).Select(t => t.Id).ToHashSet();
|
||||
if (topIds.Count > 0)
|
||||
{
|
||||
var existingIds = filteredList.Select(t => t.Id).ToHashSet();
|
||||
var extraChildren = await baseQuery
|
||||
.Where(t => t.ParentTaskId != null && topIds.Contains(t.ParentTaskId!) && !existingIds.Contains(t.Id))
|
||||
.ToListAsync(ct);
|
||||
filteredList.AddRange(extraChildren);
|
||||
}
|
||||
|
||||
filteredList = filteredList.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt).ToList();
|
||||
|
||||
var showListChip = list.Kind == ListKind.Virtual;
|
||||
foreach (var t in filteredList)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user