Skip to content

Read through a Backpex.Query - #2449

Draft
Flo0807 wants to merge 1 commit into
feature/describe-resourcefrom
feature/backpex-query
Draft

Flo0807 wants to merge 1 commit into
feature/describe-resourcefrom
feature/backpex-query

Conversation

@Flo0807

@Flo0807 Flo0807 commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Stack (merge in this order)

  1. Build queries only in the Ecto adapter #2338 Build queries only in the Ecto adapter, based on develop
  2. Let the adapter turn filter conditions and metric aggregates into queries #2340 Let the adapter turn filter conditions and metric aggregates into queries
  3. Configure filters in the LiveResource like fields #2431 Configure filters in the LiveResource like fields
  4. Describe the resource in the adapter and cast expressions with it #2448 Describe the resource in the adapter and cast expressions with it
  5. Read through a Backpex.Query #2449 Read through a Backpex.Query ← this PR
  6. Apply the parts of a query an adapter doesn't run in memory #2450 Apply the parts of a query an adapter doesn't run in memory
  7. Write through neutral changes and let adapters declare writes #2451 Write through neutral changes and let adapters declare writes
  8. Replace item_query, :select and :custom_alias with scope, load and computed values #2454 Replace item_query, :select and :custom_alias with scope, load and computed values
  9. Compute aggregates of related items #2455 Compute aggregates of related items

Step 3 of the data source abstraction: Backpex builds a neutral %Backpex.Query{} for every read, and the adapter only runs it. Adapters no longer get the fields, filters or criteria of the LiveResource.

Query

Backpex.Query holds what a read needs: from (a path of relationships, [] for the items of the resource), filter (one Backpex.Expr, already checked and cast with describe/2), search (term and target refs), sort/default_sort (refs and directions with nulls), limit, offset, load, computed, aggregates, native (data only the adapter understands), context (%{assigns: assigns}), errors and valid?.

The builder follows Ash.Query: new/2, from/2, filter/2 (ANDs), primary/2, primary_in/2, search/3, sort/2, default_sort/2, limit/2, offset/2, load/2, compute/3, aggregate/4 (named, with where:), unset/2, key/1.

Adapter contract

Before Now
get/4, list/4 read/1 (get is primary/2 + limit(1))
count/4 count/1
metric/5 aggregate/1, all aggregates of a query at once
list_options/4, list_options_by_key/4, count_options/4, options_key/3 read/1/count/1 with from: [field]; equal queries are read once (Query.key/1), and the key replaces options_key/3
fields in every read callback optional put_field_options/2: the adapter takes the options it adds to fields into query.native

Backpex.Adapters.Ecto translates its own options in put_field_options/2: :select (computed values, sorting, search), :custom_alias and association joins, preload selects, search_condition/3 of Backpex.Adapters.Ecto.Field, and the :options_query of association fields (evaluated when the query is built, so the key changes with the assigns it depends on). Backpex.Adapters.Ecto.ecto_query/1 returns the Ecto.Query of a Backpex.Query, e.g. to stream an export.

Core

  • Backpex.Resource.items_query/3, read/1, read_by_key/1, count/1, aggregate/1, metrics/3, options_query/3. get/4, reload/4, list_options/4, list_options_by_key/4, count_options/4 and options_key/3 keep their signatures and build queries.
  • Backpex.LiveResource.index_query/1 (replaces build_criteria/1), filter_query/2 and field_ref/1. index_query/1 takes the fields of the index view of the LiveResource, so it also works with the assigns of item and resource actions, which have fields of their own (e.g. an export). index_query/2 takes given fields; the index view passes its own.
  • New callback c:Backpex.LiveResource.scope/1: an expression every item of the resource matches, for every adapter. :item_query stays for Ecto.
  • New options :options_filter and :options_sort of BelongsTo, HasMany and HasManyThrough: an expression that the options match and a sort like Backpex.Query.sort/2, each also as a function of the assigns. :options_query stays for Ecto.
  • c:Backpex.Metric.data/3 receives the query of the index view (all pages) instead of criteria. Metrics the adapter computes go to it in one aggregate/1 call. A metric with ignore_filters: [:status] ignores these filters of the index view, e.g. to count the items of every status while the index view shows one; metrics that ignore the same filters share a query.
  • An invalid query (e.g. a primary value that can't be cast) reads no items. The Ecto adapter now describes :binary_id fields as Ecto.UUID, so invalid UUIDs make the query invalid instead of reaching the database.

Not in this PR

  • filter_input/2 and sort_input/2 from the design doc: the index view already validates filters and sorts from the URL. They follow when a public API needs them.
  • full_text_search stays a LiveResource option; the Ecto adapter reads it from there.
  • Steps 4 (supports?/2, in-memory fallback) and 5 (writes and the form type) follow as their own PRs on top.

Review fixes

  • 399a78b: the options query of a field with a :select runs when the options are read again, so its binding is only named for a search, as before. An options query that names its own binding raised. Its result is still part of the key, and Query.key/1 documents that adapters put what their items depend on into :native.

Merged with develop (v0.24.0)

The option checks of the relation fields from v0.24.0 run on the query: Backpex.Query.key_in/3 keeps the items whose field is one of the submitted ids (primary_in/2 uses it for the primary key), and Backpex.Resource.list_options/4 takes a :key criterion for it. The Ecto adapter keeps a limit or offset of the :options_query in a subquery when the query has a filter, so a submitted id outside the limited options is refused.

Testing

On 399a78b:

  • mix test: 1485 passed (187 doctests, 1298 tests)
  • mix test --only e2e: 60 passed
  • MIX_ENV=test mix lint: clean
  • merged coverage (measured on b75d9ed): 90.89 %

On 8f57af7 (:ignore_filters, index_query/2, :options_sort; the later merges only change the upgrade guide):

  • mix test: 1488 passed (187 doctests, 1301 tests)
  • MIX_ENV=test mix lint: clean
  • demo (on b75d9ed): bun run test 51 passed, bun run test:playwright 38 passed, bun run lint clean

@codecov

codecov Bot commented Nov 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.90444% with 12 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
lib/backpex/adapters/ecto.ex 93.63% 7 Missing ⚠️
lib/backpex/query.ex 97.05% 2 Missing ⚠️
lib/backpex/resource.ex 96.22% 2 Missing ⚠️
lib/backpex/live_resource.ex 96.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

- Read through a Backpex.Query
- Run the options query of a field when the options are read
- Keep the guides on upgrading from linking removed functions
- Build the index query with the index fields in item and resource actions
- Sort the options of association fields with :options_sort
- Let metrics ignore filters of the index view with :ignore_filters
@Flo0807
Flo0807 force-pushed the feature/backpex-query branch from 6e7ee27 to a98d1b8 Compare November 13, 2026 09:46

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change A breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant