Skip to content

Q Objects in Edgy

Edgy provides a powerful Q object system inspired by Django that enables you to build complex logical filtering expressions using natural Python boolean syntax. Q objects integrate directly with Edgy's enhanced clause engine (and_, or_, not_), support related-field lookups, wrap raw SQLAlchemy expressions, and work everywhere standard filtering clauses are accepted.

This document explains how Q objects work, why they exist, and how to use them effectively in real-world queries.

What Are Q Objects?

A Q object represents a logical filtering expression. Unlike standard keyword-based filters, which must be combined using .filter(), .and_(), or .or_(), Q objects can be combined using Python boolean operators:

  • & → logical AND
  • | → logical OR
  • ~ → logical NOT

Example:

expr = Q(name="Adam") & ~Q(email__icontains="spam")
results = await User.query.filter(expr)

Q objects allow you to express complex filtering logic concisely and readably.

Basic Usage

Using kwargs

expr = Q(name="Adam", email__icontains="edgy")
results = await User.query.filter(expr)

Equivalent to:

await User.query.filter(and_.from_kwargs(name="Adam", email__icontains="edgy"))

Using raw column expressions

expr = Q(User.columns.name == "Adam")
results = await User.query.filter(expr)

Combining Q objects

expr = Q(name="Adam") & Q(email__icontains="edgy")
results = await User.query.filter(expr)

Boolean Operators

AND (&)

expr = Q(active=True) & Q(email__icontains="edgy")

OR (|)

expr = Q(name="Adam") | Q(name="Edgy")

NOT (~)

expr = ~Q(language="PT")

Precedence

Just like Python:

  • & binds tighter than |
  • Always use parentheses for clarity

Example:

expr = Q(name="Adam") | Q(name="Edgy") & Q(language="EN")

interprets as:

Q(name="Adam") | (Q(name="Edgy") & Q(language="EN"))

Complex Expressions

Q objects are tree structures. This allows nesting:

expr = (Q(name="Adam") | Q(name="Edgy")) & ~Q(email__icontains="domain")
results = await User.query.filter(expr)

This example returns all users named Adam or Edgy, excluding those with an email from domain.

Using Q Inside QuerySets

filter()

await User.query.filter(Q(name="Adam") | Q(name="Edgy"))

or_(Q(...)) — global OR

or_ supports global OR mode when passing a single Q operand:

results = await User.query.or_(Q(name="Adam"))

Equivalent to:

results = await User.query.or_(name="Adam")

local_or(Q(...))

local_or keeps previous filters mandatory:

await User.query.filter(active=True).local_or(Q(email__icontains="gmail"))

SQL equivalent:

active = TRUE AND (email ILIKE '%gmail%')

Any field lookup you can use in normal filtering also works with Q:

expr = Q(user__id=product.user_id)
await Product.query.filter(expr)

Negation works too:

expr = ~Q(user__name="Banned")
await Product.query.filter(expr)

You can combine related lookups with boolean logic:

expr = Q(user__language="EN") | Q(user__language="PT")

Q vs. Chained and_/or_ Calls

These two are equivalent:

Q object expression

expr = Q(name="Adam") | Q(name="Edgy")
results = await User.query.filter(expr)

Long-form query builder

results = await User.query.or_(name="Adam").or_(name="Edgy")

Both generate the same SQL, but Q is easier to read and more expressive.

Q With SQLAlchemy Expressions

Q objects can wrap SQLAlchemy comparison expressions:

expr = Q(User.columns.email.contains("edgy"))
results = await User.query.filter(expr)

Or combine them:

expr = Q(User.columns.id > 10) & Q(User.columns.name.ilike("%edgy%"))

Edge Cases & Notes

Q() — empty Q

Q() is treated as a neutral AND expression.

results = await User.query.filter(Q())  # returns all rows

Q(Q(...))

Wrapping Q inside Q flattens automatically:

inner = Q(name="Adam")
expr = Q(inner) & Q(language="EN")

If a Q object wraps a clause requiring JOINs, Edgy automatically propagates the required select_related information.

Real-World Examples

Search engine style

expr = (
    Q(name__icontains="adam") |
    Q(email__icontains="adam") |
    Q(language__icontains="adam")
) & ~Q(status="banned")

results = await User.query.filter(expr)

Multi-model filtering

expr = Q(products__price__gt=100) & ~Q(language="PT")
results = await User.query.filter(expr)

Combine Q with or_ for cross-query OR

expr = Q(name="Adam")
results = await User.query.or_(expr)

Performance Notes

  • Q expressions are compiled to optimized SQLAlchemy boolean trees.
  • Edgy automatically flattens redundant AND/OR layers.
  • Prefer grouping with Q rather than chaining many .or_() calls for readability.
  • For large OR groups, consider using in_ lookups when possible:
Q(id__in=[1, 2, 3, 4])

Summary

Q objects make it easy to build readable, expressive, and powerful logical filtering conditions in Edgy. They integrate deeply with the query compiler, support related fields, wrap raw SQL expressions, and make complex filtering more maintainable.

Use Q whenever your logic requires more than a simple AND chain, or whenever you prefer a more declarative, readable style.