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:
mika kuns
2026-08-10 13:49:09 +02:00
parent 6a2a19cc9e
commit 7f10d45898
8 changed files with 152 additions and 35 deletions
@@ -0,0 +1,57 @@
using ClaudeDo.Data.Filtering.Filters;
using Microsoft.EntityFrameworkCore;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Data.Tests.Filtering;
/// <summary>
/// Proves that <see cref="ClaudeDo.Data.Filtering.ITaskListFilter.MatchExpression"/> is a real
/// expression tree that EF Core can translate into SQL, not just an in-memory delegate — the
/// whole point of splitting it out from <c>Matches</c>.
/// </summary>
public sealed class MatchExpressionSqlTests : IDisposable
{
private readonly string _dbPath;
private readonly DbContextOptions<ClaudeDoDbContext> _options;
public MatchExpressionSqlTests()
{
_dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_matchexpr_{Guid.NewGuid():N}.db");
_options = new DbContextOptionsBuilder<ClaudeDoDbContext>()
.UseSqlite($"Data Source={_dbPath}")
.Options;
using var ctx = new ClaudeDoDbContext(_options);
ctx.Database.EnsureCreated();
}
public void Dispose()
{
foreach (var suffix in new[] { "", "-wal", "-shm" })
try { File.Delete(_dbPath + suffix); } catch { }
}
[Fact]
public void StatusFilter_MatchExpression_translates_to_a_SQL_WHERE_on_status()
{
var filter = new StatusFilter("virtual:queued", TaskStatus.Queued);
using var ctx = new ClaudeDoDbContext(_options);
var sql = ctx.Tasks.Where(filter.MatchExpression).ToQueryString();
Assert.Contains("WHERE", sql, StringComparison.OrdinalIgnoreCase);
Assert.Contains("status", sql, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void UserListFilter_MatchExpression_translates_to_a_SQL_WHERE_on_list_id()
{
var filter = new UserListFilter("abc");
using var ctx = new ClaudeDoDbContext(_options);
var sql = ctx.Tasks.Where(filter.MatchExpression).ToQueryString();
Assert.Contains("WHERE", sql, StringComparison.OrdinalIgnoreCase);
Assert.Contains("list_id", sql, StringComparison.OrdinalIgnoreCase);
}
}