Skip to content

feat(search): filter on a field inside an object property - #4670

Merged
rubenvdlinde merged 2 commits into
developmentfrom
feature/nested-property-filters
Oct 16, 2026
Merged

rubenvdlinde merged 2 commits into
developmentfrom
feature/nested-property-filters

Conversation

@rubenvdlinde

@rubenvdlinde rubenvdlinde commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

What

A filter can now name a field inside an object-typed property: ?dunning.currentStage=2, ?dunning.currentStage[gte]=1, ?dunning.paused=true, or ['dunning.currentStage' => 2] from PHP.

Before this, every such query answered an empty page. PHP mangles the dot, SearchQueryHandler rebuilds the key as ['dunning' => ['currentStage' => 2]], and applyObjectFilters then read that map as a list of UUIDs. A dotted key from PHP was an unknown property and added 1 = 0.

  • New lib/Db/MagicMapper/NestedPathFilter.php:
    • It takes the path filters out of the filter map, in both spellings and at any depth.
    • It reads the path from the JSON column: #>> on PostgreSQL, JSON_UNQUOTE(JSON_EXTRACT()) on MariaDB and MySQL.
    • It writes the conditions for equality, a list of values, in, notIn, ne, isnull and like. The comparisons gt, gte, lt and lte compare as numbers when the operand is numeric; on PostgreSQL the cast is guarded so non-numeric text cannot abort the query.
  • MagicSearchHandler uses it on both the QueryBuilder path (applyObjectFilters) and the raw UNION path (buildObjectFilterConditionsSql).
  • A path segment that is not a plain name matches nothing and is listed in ignoredFilters. A path into an encrypted object property is refused, the same as a filter on that property.
  • A filter on the object property itself keeps its current meaning: a UUID, a list, or an operator bag.
  • OpenSpec change nested-property-filters, delta on zoeken-filteren.

Why now: shillinq's receivables board counts the invoices in a reminder round on ARInvoice.dunning.currentStage, and its chips filter on dunning.paused and dunning.nextDunningDate.

Tests

tests/Unit/Db/MagicSearchHandlerNestedPathFilterTest.php has 9 tests. They cover:

  • the dotted and the nested spelling;
  • operators inside a path;
  • a boolean, and a list of values;
  • an object property's own filter, which must not change;
  • an unreadable path, including an injection attempt in a segment;
  • an encrypted root;
  • the raw UNION path;
  • the MariaDB SQL;
  • split() at any depth.

5 of the 9 fail when the handler change is reverted (the other 4 test the new class directly or behaviour that has not changed). The neighbouring isnull and like suites still pass: 34 tests in total.

Live

I checked this on :8080 (PostgreSQL) as pieter, with the branch in the mount. ARInvoice has 3 rows and dunning is empty on all of them.

query before after
dunning.currentStage[isnull]=true 0 1
dunning.currentStage[isnull]=false 0 0
dunning.currentStage[gte]=1 0 0 (no error)

I also ran the generated #>> expressions, including the guarded numeric cast, against a JSON literal in the same PostgreSQL. All three returned true.

Gates (exit codes)

  • composer check:strict, one run through heavy.sh. The WSL crash at 21:34 killed it during test:all, so test:all was rerun on its own (result below).
    • lint: 0.
    • check:migration-version: 0.
    • phpcs: 0 on the changed files (6 warnings, all inherited @SPEC tags on other methods).
    • phpmd: the first run flagged StaticAccess and the complexity of conditions(). I fixed both: NestedPathFilter is now an instance collaborator, and conditions() is split into three helpers. phpmd on the two changed files then exits 0.
    • psalm: 5 errors, all inherited (OCP\DB\Schema\ITable in Migration/Version1Date20260831120000). None are in the changed files.
    • phpstan: 10 errors, all inherited (SourcesController return types, the same migration). On the changed files: [OK] No errors.
    • test:all, rerun after the crash: exit 2. Tests: 25972, Assertions: 67216, Errors: 4, Skipped: 27, Failures: 0. All 4 errors are inherited: AuditTrailMapperReadHistoryTest needs OC\DB\QueryBuilder\CompositeExpression, which is not there when the suite runs on the host without Nextcloud server core.
  • The appinfo/info.xml version is not touched. The branch is based on b08b9fe (local development), so the mount keeps the installed version; it merges cleanly into development.

AI assistance: written with Claude Code (claude-opus-5-5), reviewed by the lane coordinator.

`?dunning.currentStage=2` (and `['dunning.currentStage' => 2]` from PHP)
answered an empty page: the request layer rebuilds the mangled key into
`['dunning' => ['currentStage' => 2]]`, which applyObjectFilters read as a
list of UUIDs, and a dotted key was an unknown property that added `1 = 0`.

NestedPathFilter takes the filters on a path inside an object-typed property
out of the filter map, in both spellings and at any depth, and writes their
SQL against the JSON column (`#>>` on PostgreSQL, JSON_EXTRACT on MariaDB and
MySQL): equality, a list, in, notIn, ne, isnull, like, and gt/gte/lt/lte,
numeric when the operand is a number. The QueryBuilder path and the raw UNION
path both use it. A segment that is not a plain name matches nothing; an
encrypted object property is refused as its column filter is. A filter on the
object property itself keeps its meaning.

Assisted-by: ClaudeCode:claude-opus-5-5
Keeps both new MagicSearchHandler collaborators: development's MagicMetadataColumn and this branch's NestedPathFilter.

Assisted-by: ClaudeCode:claude-opus-5-5
@rubenvdlinde
rubenvdlinde merged commit 1c6b6df into development Oct 16, 2026
40 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ 3782b2d

Check PHP Vue Security License Tests
lint ✅
phpcs ✅
phpmd ✅
psalm ✅
phpstan ✅
phpmetrics ✅
eslint ✅
stylelint ✅
build ✅
check-specs ✅
test-l10n ✅
test-l10n-parity ✅
format ✅
check-schema-l10n ✅
composer ✅ ✅ 173/173
npm ✅ ✅ 648/648
app:check-code ⏭️
info.xml ✅
REUSE ✅
lockfile sync ✅
PHPUnit ✅ 26253/26253 · 25 skipped
Newman ✅ 714/714
Playwright ⏭️ deferred: E2E runs locally and on the promotion path only. This pull request targets development, so the suite is asked once per promotion into beta and main rather than once per push per open pull request. Run it locally with npx playwright test, or from the Actions tab on a branch with no open pull request into development.
Hydra gates ✅

Quality workflow — 2026-10-16 22:23 UTC

Download the full PDF report from the workflow artifacts.

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.

1 participant