feat(search): find a task by its label

Search covered task text only, so "regnskab" found nothing with the task
labelled @regnskab sitting in front of you — which reads as the search
box having lost it.

A label is often the only thing you remember. "That thing about the
accounts" is a search for @regnskab, not for any word in the title.

The label side is a plain lowercase match rather than a second FTS index.
Labels are short, and their names are typed by the same person now
searching for them, so there is no spelling to be generous about — the
diacritic folding that earns its keep on task text does not earn a second
index here.

The join sits inside the WHERE the visibility rules already build, so
somebody else's label cannot pull their task into your results. There is
a test for that, because it is the kind of thing an OR is good at
breaking.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kristian Wind
2026-08-17 17:27:18 +02:00
committed by Kristian Wind
co-authored by Claude Opus 5
parent 3b521b463c
commit d37d240625
2 changed files with 72 additions and 2 deletions
+17 -2
View File
@@ -136,8 +136,23 @@ func (db *DB) ListTasks(ctx context.Context, userID string, f TaskFilter) ([]Tas
args = append(args, f.DueFrom)
}
if expr := MatchExpr(f.Search); expr != "" {
where = append(where, `t.rowid IN (SELECT rowid FROM tasks_fts WHERE tasks_fts MATCH ?)`)
args = append(args, expr)
// Text or a label. A label is how you find a task whose wording you have
// forgotten — "that thing about the accounts" is a search for @regnskab —
// and leaving them out made the box look like it had lost the task.
//
// The label side is a plain lowercase match rather than another FTS index:
// labels are short, and their names are typed by the person searching for
// them, so there is no spelling to be generous about. The diacritic
// folding that matters for task text does not earn a second index here.
where = append(where, `(
t.rowid IN (SELECT rowid FROM tasks_fts WHERE tasks_fts MATCH ?)
OR t.id IN (
SELECT tl.task_id FROM task_labels tl
JOIN labels l ON l.id = tl.label_id
WHERE lower(l.name) LIKE ?
)
)`)
args = append(args, expr, "%"+strings.ToLower(strings.TrimSpace(f.Search))+"%")
}
if f.FilterSQL != "" {
where = append(where, "("+f.FilterSQL+")")