Skip to content

feat: ListQuery filter operators and multi-column ordering - #71

Open
KapplerJulien wants to merge 1 commit into
mainfrom
feat/list-query-filter-operators
Open

feat: ListQuery filter operators and multi-column ordering#71
KapplerJulien wants to merge 1 commit into
mainfrom
feat/list-query-filter-operators

Conversation

@KapplerJulien

@KapplerJulien KapplerJulien commented Sep 10, 2026

Copy link
Copy Markdown

Why

Plural DataViews backed by DatabaseModuleStorage can only express equality filters and a single ORDER BY column through ListQuery. Real listings need a little more: "current rows only" (superseded_by IS NULL), a stable secondary sort, IN lists, and range comparisons. Without them, downstream plugins keep hand-written SQL read models next to the DataView instead of a PluralObject over the TDB table.

What

  • Tangible\DataObject\Filter (new): a constraint value object with equals, not_equals, in, is_null, is_not_null, less_than, at_most, greater_than, at_least. Filter::matches() defines the reference semantics; Filter::compare() is the shared numeric-or-case-insensitive comparison rule that ordering already used.
  • ListQuery::$filters accepts a scalar (equality, unchanged), an array (shorthand for IN) or a Filter. constraints() returns the normalized Filter map for storages. $filters itself is left exactly as given, so existing readers such as (string) $query->filters['type'] keep working.
  • ListQuery ordering: orderby accepts a string (unchanged) or an array, either field => 'asc'|'desc' pairs or a list of fields sharing $order. The full ordering is exposed as $ordering; $orderby / $order stay as the first key for single-column consumers.
  • DatabaseModuleStorage translates every operator into prepared SQL and emits one ORDER BY term per ordering key. NULL handling follows the in-memory rule: <> against a non-empty value and < / <= keep NULL rows, since a stored null stringifies to '' and sorts before every value there.

Compatibility

No behaviour change for existing queries: a bare null filter value still means loose equality with '', not IS NULL. RequestRouter, PluralHandler and PluralObject are untouched; the in-memory fallback and the SQL path agree on every new scenario (see the extended parity test).

Tests

ListQuery_TestCase gains 13 tests: shorthand normalization, each operator's null and loose-equality edge cases, AND combination, multi-column ordering (array and list forms, blank/duplicate keys, unknown keys falling through) and 14 new SQL-vs-in-memory parity scenarios on a TDB table with a nullable column.

Ran locally against wordpress-develop with database-module loaded: the full suite shows the same 17 errors / 1 failure as main (Tangible Fields framework not loaded in this environment); every ListQuery and DatabaseModuleStorage test passes.

🤖 Generated with Claude Code

Plural DataViews over DatabaseModuleStorage could only express equality
filters and a single ORDER BY column, so downstream plugins kept
hand-written SQL read models next to the DataView.

- New Tangible\DataObject\Filter: equals, not_equals, in, is_null,
  is_not_null, less_than, at_most, greater_than, at_least. matches()
  defines the reference semantics; compare() is the shared ordering rule.
- ListQuery filters accept a scalar (equality, unchanged), an array (IN)
  or a Filter; constraints() returns the normalized map. $filters is
  left as given so existing readers keep working.
- ListQuery orderby accepts an array of field => direction pairs (or a
  list sharing $order), exposed as $ordering; $orderby/$order stay the
  first key.
- DatabaseModuleStorage translates each operator into prepared SQL and
  emits one ORDER BY term per key, keeping NULL rows where the in-memory
  rule keeps them.

A bare null filter value still means loose equality with '', not IS NULL.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@KapplerJulien
KapplerJulien marked this pull request as ready for review September 10, 2026 14:15
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