When expand=subtasks was used, phase 1 decided which tasks are "roots"
(top-level rows) by excluding any task whose parent lived in the same
project: `parent_tasks.project_id != tasks.project_id`. That comparison
was only ever a proxy for the real question - "is the parent actually part
of this result set?" - and the proxy keeps being wrong:
- #1000 cross-project parent
- #2494 deleted parent
- #2646 same-project parent that is filtered out
In the #2646 case a subtask that matches the filter but whose parent does
not was wrongly dropped from the roots, so a filter matching only a
subtask returned [].
Replace the proxy with the real predicate. A task is excluded from roots
only when its parent is genuinely in the matched set, i.e. ALL of:
- it has a parenttask relation, AND
- the parent task exists, AND
- the parent is within the queried project scope, AND
- the parent satisfies the active filter.
The parent-satisfies-the-filter check rebuilds the parsed filter against
the parent_tasks alias (convertFiltersToDBFilterCondWithAlias). It applies
the filter and project scope but deliberately NOT the text-search
predicate, so ParadeDB's search operators are never composed against the
parent_tasks alias.
Semantics are unchanged: count = roots. The LIMIT still slices roots in
phase 1; subtasks ride along in phase 2 (recursive CTE, deduped via NOT
IN) and are not counted toward the per-page limit. totalCount stays the
number of roots and uses the same corrected condition.
Verified on SQLite and a real ParadeDB instance (no "unsupported query
shape").
Fixes#2646