Skip to content

fix(list): match --project, --area and --tag values literally - #266

Merged
ryanlewis merged 1 commit into
mainfrom
fix/issue-262-escape-like-filters
Sep 10, 2026
Merged

ryanlewis merged 1 commit into
mainfrom
fix/issue-262-escape-like-filters

Conversation

@ryanlewis

Copy link
Copy Markdown
Owner

Closes #262

What changed

The list filters matched titles with LIKE and passed the value straight in as the pattern. things --project '%' matched every project — and since #260, lifted the closed-project fold for all of them at once — and a title holding a % or a _ matched more than itself.

Filter values are now escaped for %, _ and the escape character, and every LIKE that takes one declares ESCAPE. Matching stays case-insensitive, which is what a name filter wants and what it did before; only the wildcards lose their meaning. The uuid arm keeps the raw value, since it is an equality test rather than a pattern.

things projects --area takes the same flag and is escaped the same way, so the two commands cannot disagree about what a name matches.

The golden file bounds it

100 of the 200 combinations moved: exactly the five filter-carrying shapes across ten views and both --include-completed states. No combination without a filter moved, and every one of the 100 differs only by the added ESCAPE clause — checked mechanically, line by line, not by eye.

Measured against a base build

Built origin/main and compared on the live database:

command base this branch
--project '%' 540 rows 0
--area 'Personal Projects' 43 43
anytime 108 108

No project, area or tag title in that database contains a % or a _, so no real name changes behaviour.

A worse instance of the same bug, left for its own issue

/code-review found that FindTasksByTitle and SearchTasks wrap the raw value in %…% with no escaping. That is the same gap on a more dangerous surface, because GetTask falls back to FindTasksByTitle and auto-selects when exactly one row comes back — so things complete '20_30 review' can resolve to and complete a task titled 20:30 review. There are 4 open tasks with an underscore in the title on this database.

The search half is live and measurable today: things search '50%' returns 45 rows, while the number of tasks whose title or notes contain the literal text 50% is 0. It is matching %50%%.

Not fixed here. Escaping those changes what show, complete and search match, on a different command surface from the filters, and needs its own tests and a docs note — the README documents <task> as taking a title substring. Worth an issue.

Docs

None needed. The README already describes these flags as matching a name or UUID, which is what they now actually do; the pattern behaviour was accidental. The <task> substring claim is about the lookup path, which this does not touch.

make test and make lint are clean.

The list filters matched titles with LIKE and passed the value straight in as
the pattern. So `things --project '%'` matched every project — and since #260
lifted the closed-project fold for all of them at once — and a title holding a
% or a _ matched more than itself.

Filter values are now escaped for %, _ and the escape character, and every
LIKE that takes one declares ESCAPE. Matching stays case-insensitive, which is
what a name filter wants and what it did before; only the wildcards lose their
meaning. The uuid arm keeps the raw value, since it is an equality test rather
than a pattern.

`things projects --area` takes the same flag and is escaped the same way, so
the two commands cannot disagree about what a name matches.

The golden SQL file is the record: 100 of the 200 view-and-filter combinations
moved, which is exactly the five filter-carrying shapes across ten views and
both --include-completed states, and every one of the 100 differs only by the
added ESCAPE clause. No combination without a filter moved.

Measured against a build of origin/main on the live database: `--project '%'`
went from 540 rows to none, while `--area 'Personal Projects'` returned the
same 43 either way. No project, area or tag title in that database contains a
% or a _, so no real name changes behaviour.

The lookup and search paths are deliberately left alone. They wrap the value
in %…% because substring matching is what `things show <title>` and
`things search` are for. They have the same escaping gap, which is worse there
than it was here, but fixing it changes what those commands match and belongs
with its own tests and its own issue.

Closes #262
@ryanlewis
ryanlewis merged commit 51eac02 into main Sep 10, 2026
8 checks passed
@ryanlewis
ryanlewis deleted the fix/issue-262-escape-like-filters branch September 10, 2026 14:58
ryanlewis added a commit that referenced this pull request Sep 10, 2026
Closes #267

## Release note

**A task reference and a search query now match literally — `%` and `_`
are characters to find, not wildcards — so `things complete` can no
longer resolve to a task you did not name.**

## What changed

`FindTasksByTitle` and `SearchTasks` matched with `LIKE` and used the
caller's text as the pattern, so `_` stood for any character and `%` for
any run of them. `GetTask` falls back to `FindTasksByTitle` and acts on
a lookup that matches exactly one row, so `things complete '20_30
review'` could resolve to, and complete, a task called `20:30 review`.
#266 fixed the same gap for the list filters; this is the more dangerous
half, because a write command acts on the result.

Both now escape the value and declare `ESCAPE`, from the constant #266
introduced. Substring matching is unchanged — the wildcards either side
are the CLI's, and only the characters inside the value stop being
pattern syntax. Matching stays case-insensitive.

## Measured on the live database

| command | before | after |
| --- | --- | --- |
| `search '50%'` | 45 rows | 0 |
| items whose title or notes contain the literal text `50%` | 0 | 0 |
| `search 'review'` | 129 | 129 |

## My first regression tests were worthless, and that is the part worth
reading

Both passed against the **unfixed** code. `/code-review` caught it by
reverting the fix and running them. Two separate mistakes, both easy to
repeat:

- `GetTask` resolves an exact title before the `LIKE` is ever reached,
so seeding the exact title under test never exercised the path.
- `LIKE` wildcards act on the **pattern**, not on the stored value. I
put the underscore in the seeded title and queried plain text, which
tests nothing.

They are rebuilt around the real failure: resolve a reference that must
miss, then resolve a substring that forces the `LIKE` path. I reverted
`tasks.go` and confirmed they now fail, rather than taking the review's
word:

```
GetTask("20_30 review") resolved to "t-colon"; no task is called that
GetTask("20_30 rev"): ambiguous task: "20_30 rev" matches 2 tasks
resolved to "t-colon" — a write would have hit a task the user never named
search 50%: got [s-pct s-fifty s-note], want [s-pct s-note]
search a_b: got [s-under s-axb], want [s-under]
```

## Docs

Nothing documented changes, because wildcards were never offered for
`<task>` or `search`. The README, the site's Searching and Inspecting
sections, and the bundled skill now say the matching is literal, so the
absence is stated rather than assumed.

`make test` and `make lint` are clean; `hugo` builds the site.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: --project, --area and --tag filter values are used as LIKE patterns unescaped

1 participant