Skip to content

BDMS 774 Filtering and Sorting Wells & Contacts#665

Merged
jeremyzilar merged 14 commits into
stagingfrom
BDMS-774----well-list-filtering
Apr 28, 2026
Merged

BDMS 774 Filtering and Sorting Wells & Contacts#665
jeremyzilar merged 14 commits into
stagingfrom
BDMS-774----well-list-filtering

Conversation

@jeremyzilar

Copy link
Copy Markdown
Contributor

What is changing

List pages mix two kinds of columns:

  1. Real SQL columns on the main row (or a join the API already treats as straightforward). Filters and sorts map directly to the database.
  2. Everything else: association summaries, “latest” values, labels built from several tables. Those are not one column you can filter or sort with a naive WHERE / ORDER BY.

This work applies to both Contacts (GET /contact and related) and Wells (GET /thing/... water well style lists). In both places, when the grid needs (2), the API mimics a real column: stable names in the query string, then server side SQL that matches the intent.

Filters

Virtual filter fields map UI field names to SQL that answers “does any related row match?” without duplicating parent rows (mostly EXISTS). Examples: contacts on wells, things on contacts.

Repeated filter= parameters (Refine or DataGrid style) are decoded one by one and combined with AND. Sending one filter is still a list of length one. Sending none adds no filter predicates from that mechanism.

image

Sorts

Virtual sort keys map UI sort names to one expression per row used in ORDER BY (for example minimum linked name for an association, latest text from a history table). Implemented in _apply_thing_virtual_sort and _apply_contact_virtual_sort in services/query_helper.py. Details and tables live in docs/refine-json-filters-and-virtual-fields.md.

Why it mattered: Sorting on those names used to treat non-column attributes like database columns and could 500. Those paths are now defined in SQL.

Stricter validation

Filter JSON must include field, operator, and value. Missing keys return 422.


Is this a good idea?

Alternate approaches

  • Keep logic in the API (current direction): virtual names plus documented SQL.
  • Move shape into the database (views, generated columns, or denormalized fields) so more list columns become “real” in SQL.
  • Change the contract (separate endpoints or a different filter or sort dialect for heavy association columns).

What changed (short)

  • Multiple filter= params: AND together.
  • Virtual filters: contacts (wells), things (contacts), plus existing derived status filter paths.
  • Virtual sorts: derived Thing and Contact list fields that are not plain columns; behavior documented in docs/refine-json-filters-and-virtual-fields.md.
  • 422 when a filter object is missing required keys.

Docs

  • docs/refine-json-filters-and-virtual-fields.md (filters, sorts, operators, semantics).

jeremyzilar and others added 14 commits April 27, 2026 18:01
…n join

Document Thing virtual filter fields in query_helper. Add contacts filter
matching any linked Contact.name with contains, ncontains, eq, ne,
startswith, endswith.
- Accept repeated filter query params via order_sort_filter merge of filters list
- Contact virtual filter field things: EXISTS join ThingContactAssociation and
  Thing.name for contains/null operators (inverse of Thing contacts filter)
- Thing virtual sorts for properties and proxies that cannot use ORDER BY on ORM
  attributes: monitoring_status, well_status, datalogger_suitability_status,
  site_name (NMBGMR thing_id_link), contacts and aquifers (min lower name),
  open_status (CASE on Open Status history), measuring_point_height (latest MP row)
- Contact virtual sort for things: min lower Thing.name across associations
- Document dispatch order in _apply_json_filter_clause and module header
- Add sqlalchemy case and nulls_last imports
- Switch get_db_contacts to keyword-only filters argument matching
  order_sort_filter(filters=...)
- Document virtual things filter and get_db_contacts link to list endpoint
- Split add_contact signature across lines for line length
- Replace single filter string with Annotated list from Query(alias=filter)
  so Refine can send multiple JSON filter objects (AND semantics)
- Point get_contacts docstring at virtual things filter and get_db_contacts
- Add typing Annotated import for filter_params
- things contains, no match, and nnull filter cases
- Multiple filter parameters AND together (things plus name)
- Sort by things (min site name) returns 200 and includes linked contact
- Module-scoped auth override matching other API tests
- monitoring_status and well_status descending or ascending
- site_name, contacts, open_status, measuring_point_height, aquifers
  sort smoke tests (expect 200, SQL-backed ORDER BY)
- Covers Thing virtual sort keys used by the wells DataGrid
- Features bullet notes repeated filter params and virtual association fields
- Points readers to docs/refine-json-filters-and-virtual-fields.md for rationale
- New subsection under architecture for DataGrid filter and sort wiring
- Cross-link to refine-json-filters-and-virtual-fields.md for maintainers
- Explains repeated filter query params, virtual filter fields, EXISTS pattern,
  and inverse Thing contacts versus Contact things associations
- Documents sorting for non-column Thing and Contact fields including scalar
  summaries (min names, StatusHistory subqueries, site_name via thing_id_link)
- Tables map API behavior to query_helper helpers; notes tests to read

Note: docs/ is gitignored by default; this file is tracked via force-add so the
guide stays in version control alongside query_helper.
@jeremyzilar jeremyzilar merged commit ce1e533 into staging Apr 28, 2026
6 checks passed
@jeremyzilar jeremyzilar deleted the BDMS-774----well-list-filtering branch April 28, 2026 15:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants