Skip to content

Add Foreign Key Filter Support in SearchBuilder#745

Open
Alwinator wants to merge 2 commits into
jowilf:mainfrom
Alwinator:main
Open

Add Foreign Key Filter Support in SearchBuilder#745
Alwinator wants to merge 2 commits into
jowilf:mainfrom
Alwinator:main

Conversation

@Alwinator

Copy link
Copy Markdown
Contributor

Related Issues

What this does

This PR adds the ability to filter by foreign key (relation) fields directly in the DataTables SearchBuilder UI. Previously, relation fields (HasOne / HasMany) only supported null / !null conditions in the SearchBuilder. Now they behave like string columns and support the full set of string conditions: =, !=, contains, starts, ends, and their negated counterparts.

When a user types a search term (e.g. "John") for a relation column, the backend resolves it by searching across all string columns of the related model using case-insensitive matching. This effectively matches against what __admin_repr__ would show — so the filter feels intuitive from the user's perspective.

How it works

  • The SearchBuilder treats relation fields as string type columns, so users get a free-text input (no dropdown, no extra AJAX calls).
  • On the backend, when a string operator hits a relationship attribute, we introspect the related model's string columns and build .has() (for HasOne) or .any() (for HasMany) clauses with OR'd ilike conditions.
  • Negated operators (not_contains, neq, etc.) use AND logic so that a row is only excluded if none of the related model's string columns match.
  • is_null / is_not_null continue to work exactly as before.

No frontend JS changes were needed — the existing string SearchBuilder type and extractCriteria() handle everything already since we're in serverSide: true mode.

Performance escape hatch

For large datasets where searching all string columns on a related model might be expensive, there's a new searchable_relation_fields option:

class PostView(ModelView):
    searchable_relation_fields = {
        "publisher": ["last_name", "first_name"],
    }

This restricts the generated query to only the specified columns instead of scanning every string column on the related model. By default (when not set), all string columns are searched.

Changes

  • starlette_admin/fields.pyRelationField.search_builder_type changed from "default" to "string"
  • starlette_admin/views.py — Added searchable_relation_fields attribute to BaseModelView
  • starlette_admin/contrib/sqla/view.py — Relation fields are now included in the default searchable_fields list (but still excluded from sortable_fields); searchable_relation_fields is threaded through to the query builder
  • starlette_admin/contrib/sqla/helpers.py — New _relationship_string_filter() and _apply_string_operator() helpers; build_query() now detects string operators on relationship attributes and delegates accordingly
  • tests/sqla/test_view_meta.py — Updated assertions to reflect that relation fields are now searchable by default
  • tests/sqla/test_fk_filter.py — 28 new tests covering HasOne filters, HasMany filters, AND/OR combinations, the searchable_relation_fields allowlist, reverse relations, case-insensitive matching, and multi-column search

Scope

This PR only implements FK filtering for the SQLAlchemy backend (including SQLModel, which inherits it automatically). Support for ODMantic, MongoEngine, and Beanie is deferred to a follow-up — their converters would need similar treatment but the patterns differ enough to warrant separate work.

Testing

All existing SQLA tests continue to pass. The 28 new tests cover:

  • Every string operator on HasOne relations (contains, eq, neq, startswith, endswith, and all negations)
  • HasMany relation filtering (uses .any() instead of .has())
  • is_null / is_not_null (unchanged behavior, but verified alongside the new operators)
  • AND/OR combinations mixing FK filters with regular field filters
  • The searchable_relation_fields allowlist (verifying that excluded columns are truly not searched)
  • Reverse relation filtering (e.g. filtering Publishers by their Books)
  • Case-insensitive matching
  • Multi-column search (matching against any string column on the related model)

@Alwinator Alwinator reopened this Apr 2, 2026
@codecov

codecov Bot commented Apr 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.57576% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 99.88%. Comparing base (0f684fe) to head (ee48fe0).

Files with missing lines Patch % Lines
starlette_admin/contrib/sqla/helpers.py 87.09% 8 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##              main     #745      +/-   ##
===========================================
- Coverage   100.00%   99.88%   -0.12%     
===========================================
  Files           86       87       +1     
  Lines         6848     7173     +325     
===========================================
+ Hits          6848     7165     +317     
- Misses           0        8       +8     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jowilf

jowilf commented Apr 15, 2026

Copy link
Copy Markdown
Owner

Thanks @Alwinator for the PR, i like the proposition. I will review the code and merge if it looks good

@jowilf jowilf added this to the 0.17.0 milestone Apr 16, 2026
@jowilf jowilf removed this from the 0.17.0 milestone Jun 6, 2026
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.

Enhancement: Foreign Key filter

2 participants