Filter Expressions
pypaginate provides a type-safe filter system based on FilterSpec models,
And/Or group builders, and a compile-once predicate engine. The same filter
specs work for both in-memory and SQLAlchemy backends.
FilterSpec
A FilterSpec is an immutable Pydantic model that describes a single filter condition:
from pypaginate import FilterSpec
# Equality (default operator)
FilterSpec(field="status", value="active")
# Comparison
FilterSpec(field="age", operator="gte", value=18)
# Pattern matching
FilterSpec(field="name", operator="contains", value="john")
# Null check
FilterSpec(field="deleted_at", operator="is_null")
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
required |
Field name (supports nested: |
|
|
|
Operator name (Literal type, validated at definition) |
|
|
|
Comparison value |
|
|
|
How to combine with other specs |
Operators
pypaginate supports 20 operators, all type-checked as Literal values:
Comparison
Operator |
SQL Equivalent |
Example |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Membership
Operator |
SQL Equivalent |
Example |
|---|---|---|
|
|
|
|
|
|
|
|
|
Text / Pattern
Operator |
SQL Equivalent |
Example |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Null / Existence
Operator |
SQL Equivalent |
Example |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
(always true) |
|
And/Or Groups (FilterGroup)
For complex boolean logic, use the And() and Or() builder functions to create
nested FilterGroup trees:
from pypaginate import FilterSpec, And, Or
# (status = "active") AND (age >= 18 OR role = "admin")
group = And(
FilterSpec(field="status", value="active"),
Or(
FilterSpec(field="age", operator="gte", value=18),
FilterSpec(field="role", value="admin"),
),
)
Groups can be nested up to 5 levels deep (validated by Pydantic):
# Complex nested expression
group = And(
Or(
FilterSpec(field="a", value=1),
FilterSpec(field="b", value=2),
),
Or(
FilterSpec(field="c", value=3),
And(
FilterSpec(field="d", operator="gte", value=10),
FilterSpec(field="e", operator="lte", value=20),
),
),
)
# SQL: (a=1 OR b=2) AND (c=3 OR (d>=10 AND e<=20))
How Filtering Works
In-Memory (FilterEngine)
The FilterEngine compiles filter specs into fast predicate closures once,
then evaluates them per item with minimal overhead:
graph LR
S["FilterSpec list or FilterGroup"] --> C["Compile predicates (once)"]
C --> P["predicate closures"]
P --> E["Evaluate per item"]
E --> R["Filtered list"]
from pypaginate.filtering.engine import FilterEngine
engine = FilterEngine()
filtered = engine.apply(items, [
FilterSpec(field="age", operator="gte", value=18),
FilterSpec(field="status", value="active"),
])
For flat FilterSpec lists, the engine partitions specs by logic (AND vs OR):
All AND specs must match.
At least one OR spec must match (if any exist).
For FilterGroup trees, the engine recursively compiles nested predicates.
SQLAlchemy (SQLAlchemyFilterBackend)
The SQLAlchemy filter backend translates FilterSpec objects into SQLAlchemy
WHERE clauses:
from pypaginate.adapters.sqlalchemy import SQLAlchemyFilterBackend
fb = SQLAlchemyFilterBackend()
modified_query = fb.apply_filters(
select(User),
[FilterSpec(field="age", operator="gte", value=18)],
)
# Generates: SELECT * FROM users WHERE users.age >= 18
Pipeline Integration
Both backends integrate with the pipeline for filter + sort + search + paginate:
from pypaginate.engine.pipeline import AsyncPipeline
from pypaginate.engine.paginator import AsyncPaginator
from pypaginate.adapters.sqlalchemy import SQLAlchemyBackend, SQLAlchemyFilterBackend
pipeline = AsyncPipeline(
AsyncPaginator(SQLAlchemyBackend(session)),
filter_backend=SQLAlchemyFilterBackend(),
)
result = await pipeline.execute(
select(User),
OffsetParams(page=1, limit=20),
filters=[FilterSpec(field="status", value="active")],
)
Nested Field Access
The filter engine supports dotted paths for nested objects:
# Access nested attribute: item.address.city
FilterSpec(field="address.city", operator="eq", value="Paris")
The compile_accessor function compiles a dotted path into a fast callable that
resolves the value from any object (dict or object with attributes).
Operators
All 20 operators are implemented in the bundled native _core engine. They are
not Python-registrable: there is no public operator registry, and FilterEngine()
takes no constructor arguments. To filter with logic not covered by a built-in
operator, filter at the application layer, or open an issue to add the operator
to the core.
Error Handling
Filter errors carry structured details:
from pypaginate.domain.exceptions import FilterError, FilterValidationError
try:
engine.apply(items, [FilterSpec(field="x", operator="regex", value="[invalid")])
except FilterError as e:
print(e.details) # {"pattern": "[invalid", "error": "..."}
try:
FilterSpec(field="age", operator="between", value=[1, 2, 3])
except FilterValidationError as e:
print(e.details) # {"value": "[1, 2, 3]", "length": 3}