Skip to content

Query Builder - Advanced usage

This page covers advanced programmatic filtering for developers who need to build filters dynamically in Python code, rather than using the HTTP header syntax.

Programmatic filtering

FastEdgy provides filter classes that allow you to construct filters programmatically with full type safety and IDE autocomplete support.

Why use programmatic filters

Use programmatic filters when you need to:

  • Build complex filters dynamically based on business logic
  • Reuse filter expressions across multiple queries
  • Leverage IDE autocomplete and type checking
  • Create custom API endpoints with filtering logic
  • Combine multiple filter sources programmatically

Available classes

FastEdgy provides the following classes for programmatic filtering:

Class Purpose Example
R Create a single filter rule R("name", "=", "value")
FilterRule Same as R (verbose form) FilterRule("name", "=", "value")
And Combine rules with AND logic And(rule1, rule2)
Or Combine rules with OR logic Or(rule1, rule2)
FilterCondition Base class for conditions Used internally

Basic usage

Simple filter rule

Use the R class (short for Rule) to create individual filter expressions:

from fastedgy.api_route_model.params import R, filter_query
from myapp.models import Product

# Create a query
query = Product.query

# Apply a simple filter
filtered_query = filter_query(query, R("name", "=", "Laptop"))

# Execute the query
products = await filtered_query.all()

Using FilterRule

R is an alias for FilterRule. Both are identical:

from fastedgy.api_route_model.params import FilterRule, filter_query

# These are equivalent
filter1 = R("price", ">", 100)
filter2 = FilterRule("price", ">", 100)

Combining filters

AND conditions

Combine multiple rules that must all be true:

from fastedgy.api_route_model.params import R, And, filter_query
from myapp.models import Product

query = Product.query

# Products that are active AND price >= 100
filters = And(
    R("is_active", "is true"),
    R("price", ">=", 100)
)

filtered_query = filter_query(query, filters)
products = await filtered_query.all()

OR conditions

Combine multiple rules where at least one must be true:

from fastedgy.api_route_model.params import R, Or, filter_query
from myapp.models import Product

query = Product.query

# Products in electronics OR books category
filters = Or(
    R("category.slug", "=", "electronics"),
    R("category.slug", "=", "books")
)

filtered_query = filter_query(query, filters)
products = await filtered_query.all()

Advanced patterns

Nested conditions

Combine AND and OR conditions for complex logic:

from fastedgy.api_route_model.params import R, And, Or, filter_query
from myapp.models import Product

query = Product.query

# Active products AND (cheap OR on sale)
filters = And(
    R("is_active", "is true"),
    Or(
        R("price", "<", 50),
        R("category.slug", "=", "sale")
    )
)

filtered_query = filter_query(query, filters)
products = await filtered_query.all()

Reusable filter expressions

Define filters once and reuse them:

from fastedgy.api_route_model.params import R, And, filter_query
from myapp.models import Product

# Define reusable filters
active_filter = R("is_active", "is true")
affordable_filter = R("price", "<=", 100)
featured_filter = R("is_featured", "is true")

# Combine them in different ways
query1 = Product.query
affordable_active = await filter_query(query1, And(active_filter, affordable_filter)).all()

query2 = Product.query
featured_active = await filter_query(query2, And(active_filter, featured_filter)).all()

Dynamic filter construction

Build filters based on runtime conditions:

from fastedgy.api_route_model.params import R, And, filter_query
from myapp.models import Product

async def get_products_by_criteria(
    min_price: float | None = None,
    max_price: float | None = None,
    category: str | None = None,
):
    query = Product.query
    rules = [R("is_active", "is true")]

    if min_price is not None:
        rules.append(R("price", ">=", min_price))

    if max_price is not None:
        rules.append(R("price", "<=", max_price))

    if category is not None:
        rules.append(R("category.slug", "=", category))

    filters = And(*rules) if len(rules) > 1 else rules[0]
    filtered_query = filter_query(query, filters)
    return await filtered_query.all()

# Use it
products = await get_products_by_criteria(min_price=50, category="electronics")

Using filter_query helper

The filter_query helper function accepts multiple filter formats:

Supported input formats

from fastedgy.api_route_model.params import R, And, filter_query
from myapp.models import Product

query = Product.query

# 1. Filter classes (NEW - what this page is about)
filter_query(query, R("name", "=", "Laptop"))
filter_query(query, And(R("price", ">", 100), R("is_active", "is true")))

# 2. Tuple format
filter_query(query, ("name", "=", "Laptop"))
filter_query(query, ("&", [("price", ">", 100), ("is_active", "is true")]))

# 3. JSON string format
filter_query(query, '["name", "=", "Laptop"]')
filter_query(query, '["&", [["price", ">", 100], ["is_active", "is true"]]]')

# 4. None (no filtering)
filter_query(query, None)

Custom endpoints with filtering

Use programmatic filters in custom API endpoints:

from fastapi import APIRouter, Depends
from fastedgy.api_route_model.params import R, And, filter_query
from myapp.models import Product
from myapp.schemas import ProductSchema

router = APIRouter()

@router.get("/api/products/featured")
async def get_featured_products(
    category: str | None = None,
):
    query = Product.query

    # Build filter dynamically
    rules = [
        R("is_active", "is true"),
        R("is_featured", "is true"),
    ]

    if category:
        rules.append(R("category.slug", "=", category))

    filters = And(*rules)
    query = filter_query(query, filters)

    products = await query.all()
    return [ProductSchema.model_validate(p) for p in products]

Type safety benefits

Using filter classes provides several advantages over string-based filters:

IDE autocomplete

Your IDE can suggest available methods and detect typos:

from fastedgy.api_route_model.params import R

# IDE will autocomplete field names and show you the signature
rule = R(
    field="price",      # Autocomplete suggests field names
    operator=">=",      # Autocomplete suggests valid operators
    value=100
)

Type checking

Static type checkers like mypy can validate your filter expressions:

from fastedgy.api_route_model.params import R, And

# Type checker validates this
filters = And(
    R("name", "=", "test"),
    R("price", ">", 100)
)

# Type checker catches this error (wrong type)
# filters = And("invalid", 123)  # Type error!

Refactoring safety

When you rename fields in your models, you can use IDE refactoring tools to update filter expressions automatically.

Combining with HTTP filters

You can combine programmatic filters with HTTP header filters using merge_filters:

from fastedgy.api_route_model.params import (
    R,
    And,
    filter_query,
    merge_filters,
    parse_filter_input,
    FilterHeader,
)
from fastapi import Depends
from myapp.models import Product

@router.get("/api/products")
async def list_products(
    x_filter: str | None = Depends(FilterHeader()),
):
    query = Product.query

    # Parse user's filter from header
    user_filters = parse_filter_input(x_filter) if x_filter else None

    # Add mandatory filter (always active products)
    system_filter = R("is_active", "is true")

    # Merge both filters with AND logic
    combined = merge_filters(system_filter, user_filters)

    # Apply to query
    query = filter_query(query, combined)
    products = await query.all()

    return products

All supported operators

Programmatic filters support the same operators as HTTP filters. See the Usage Guide for the complete list of operators for each field type.

Quick reference

from fastedgy.api_route_model.params import R

# Comparison
R("price", "=", 100)
R("price", "!=", 100)
R("price", "<", 100)
R("price", "<=", 100)
R("price", ">", 100)
R("price", ">=", 100)
R("price", "between", [50, 150])

# Text search
R("name", "like", "%laptop%")
R("name", "ilike", "%laptop%")
R("name", "starts with", "Mac")
R("name", "ends with", "Pro")
R("name", "contains", "book")
R("name", "icontains", "book")

# Lists
R("status", "in", ["pending", "active"])
R("status", "not in", ["archived", "deleted"])

# Boolean
R("is_active", "is true")
R("is_featured", "is false")

# Null checks
R("deleted_at", "is empty")
R("description", "is not empty")

# Relations
R("category.name", "=", "Electronics")
R("tags", "in", [1, 2, 3])

# Spatial (PostGIS)
R("location", "spatial within distance", [[2.3522, 48.8566], 5000])
R("location", "spatial distance <", [[2.3522, 48.8566], 10000])

# Vector (pgvector)
R("embedding", "cosine distance <", [0.1, [0.2, 0.3, 0.4]])

Error handling

Filter classes validate operators at instantiation:

from fastedgy.api_route_model.params import R

try:
    # Invalid operator
    rule = R("name", "invalid_operator", "value")
except ValueError as e:
    print(f"Error: {e}")
    # Output: Error: Operator 'invalid_operator' is not supported

Field validation happens when you apply filters to a query:

from fastedgy.api_route_model.params import R, filter_query, InvalidFilterError
from myapp.models import Product

try:
    query = Product.query
    # Invalid field name
    filtered = filter_query(query, R("nonexistent_field", "=", "value"))
    await filtered.all()
except InvalidFilterError as e:
    print(f"Error: {e}")
    # Output: Error: Invalid filter field: nonexistent_field

Best practices

Use R for conciseness

Prefer R over FilterRule for shorter, more readable code:

# Good
filters = And(
    R("is_active", "is true"),
    R("price", ">=", 100)
)

# Works but more verbose
filters = And(
    FilterRule("is_active", "is true"),
    FilterRule("price", ">=", 100)
)

Extract complex filters to functions

For complex filter logic, create dedicated functions:

from fastedgy.api_route_model.params import R, And, Or

def get_premium_product_filter():
    """Products that are premium: high price OR featured."""
    return Or(
        R("price", ">=", 1000),
        R("is_featured", "is true")
    )

def get_available_filter():
    """Products that are available for purchase."""
    return And(
        R("is_active", "is true"),
        R("stock", ">", 0)
    )

# Use in queries
query = Product.query
filters = And(
    get_available_filter(),
    get_premium_product_filter()
)
filtered_query = filter_query(query, filters)
products = await filtered_query.all()

Document field paths for relations

When filtering by related fields, document the relationship path:

from fastedgy.api_route_model.params import R

# Document the relationship for future maintainers
# Product -> Category (ForeignKey) -> name (CharField)
category_filter = R("category.name", "=", "Electronics")

# Product -> Tags (ManyToMany) -> id (IntegerField)
tags_filter = R("tags", "in", [1, 2, 3])

Next steps

Now that you understand programmatic filtering, you might want to explore:

Back to Overview