Smart Query Mixin
The SmartQueryMixin
class provides advanced query functionality for SQLAlchemy
models, allowing you to filter, sort, group and eager load data in a single
query, making it easier to retrieve specific data from the database.
It uses the functionality of the Inspection Mixin
.
Info
This mixin is intended to extend the functionality of the
Active Record Mixin
which the
examples below are based on. It also extends the functionality
of the Async Query
wrapper. It is not
intended to be used on its own.
Warning
All relations used in filtering/sorting/grouping should be explicitly set,
not just being a backref
. See the
About Relationships section
for more information.
Info
The examples below assume the following models:
from sqlalchemy import ForeignKey, String
from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlactive.base_model import ActiveRecordBaseModel
class BaseModel(ActiveRecordBaseModel):
__abstract__ = True
class User(BaseModel):
__tablename__ = 'users'
id: Mapped[int] = mapped_column(
primary_key=True, autoincrement=True, index=True
)
username: Mapped[str] = mapped_column(
String(18), nullable=False, unique=True
)
name: Mapped[str] = mapped_column(String(50), nullable=False)
age: Mapped[int] = mapped_column(nullable=False)
posts: Mapped[list['Post']] = relationship(back_populates='user')
comments: Mapped[list['Comment']] = relationship(back_populates='user')
@hybrid_property
def is_adult(self) -> int:
return self.age > 18
@hybrid_method
def older_than(self, other: 'User') -> bool:
return self.age > other.age
class Post(BaseModel):
__tablename__ = 'posts'
id: Mapped[int] = mapped_column(
primary_key=True, autoincrement=True, index=True
)
title: Mapped[str] = mapped_column(String(100), nullable=False)
body: Mapped[str] = mapped_column(nullable=False)
rating: Mapped[int] = mapped_column(nullable=False)
user_id: Mapped[int] = mapped_column(ForeignKey('users.id'))
user: Mapped['User'] = relationship(back_populates='posts')
comments: Mapped[list['Comment']] = relationship(back_populates='post')
class Comment(BaseModel):
__tablename__ = 'comments'
id: Mapped[int] = mapped_column(
primary_key=True, autoincrement=True, index=True
)
body: Mapped[str] = mapped_column(nullable=False)
post_id: Mapped[int] = mapped_column(ForeignKey('posts.id'))
user_id: Mapped[int] = mapped_column(ForeignKey('users.id'))
post: Mapped['Post'] = relationship(back_populates='comments')
user: Mapped['User'] = relationship(back_populates='comments')
class Product(BaseModel):
__tablename__ = 'products'
id: Mapped[int] = mapped_column(
primary_key=True, autoincrement=True, index=True
)
name: Mapped[str] = mapped_column(String(100), nullable=False)
description: Mapped[str] = mapped_column(String(100), nullable=False)
price: Mapped[float] = mapped_column(nullable=False)
sells: Mapped[list['Sell']] = relationship(
back_populates='product', viewonly=True
)
class Sell(BaseModel):
__tablename__ = 'sells'
id: Mapped[int] = mapped_column(primary_key=True)
product_id: Mapped[int] = mapped_column(
ForeignKey('products.id'), primary_key=True
)
quantity: Mapped[int] = mapped_column(nullable=False)
product: Mapped['Product'] = relationship(back_populates='sells')
Core Features
Smart Queries
Smart queries allow you to filter, sort, group and eager load data in a single query.
users = await User.smart_query(
criteria=(User.age >= 18,),
filters={'name__like': '%Bob%'},
sort_columns=(User.username,),
sort_attrs=['-created_at'],
group_columns=(User.username,),
group_attrs=['age'],
schema={User.posts: 'joined'}
).all()
Filtering
You can filter data using native SQLAlchemy filter expressions.
Also, you can filter data using Django-like filter expressions.
Sorting
You can sort data using native SQLAlchemy sort expressions with
the sort_columns
parameter.
Also, you can sort data using Django-like sort expressions with
the sort_attrs
parameter.
Grouping
You can group data using native SQLAlchemy group expressions with
the group_columns
parameter.
Also, you can group data using Django-like group expressions with
the group_attrs
parameter.
Eager Loading
You can eager load relationships using various loading strategies
with the schema
parameter.
Searching
You can search data using the search
method.
users = await User.search(
query=User.query,
search_term='Bob',
columns=(User.name, User.username),
).all()
API Reference
The SmartQueryMixin
class provides three low-level methods for building
filter, sort, group and eager load expressions:
filter_expr
: Builds filter expressions.order_expr
: Builds order expressions.columns_expr
: Builds column expressions.eager_expr
: Builds eager load expressions.smart_query
: Builds a smart query (filter, sort, group and eager load).apply_search_filter
: Applies a search filter to the query.
Filter Operators
The SmartQueryMixin
class provides a set of Django-like filter operators
for building filter expressions.
isnull
Whether the value is null.
exact
Equal to.
eq
Same as
exact
.
ne
Not equal to.
gt
Greater than.
ge
Greater than or equal to.
lt
Less than.
le
Less than or equal to.
in
Included in.
notin
Not included in.
between
Inside a range.
like
SQL
LIKE
clause.
ilike
Case-insensitive SQL
LIKE
clause for PostgreSQL.When used with other backends, such as MySQL, is the same as
like
.
startswith
Start with.
istartswith
Case-insensitive start with.
endswith
End with.
iendswith
Case-insensitive end with.
contains
Contains a substring (case-insensitive).
year
Date year is equal to.
year_ne
Date year is not equal to.
year_gt
Date year is greater than.
year_ge
Date year is greater than or equal to.
year_lt
Date year is less than.
year_le
Date year is less than or equal to.
month
Date month is equal to.
month_ne
Date month is not equal to.
month_gt
Date month is greater than.
month_ge
Date month is greater than or equal to.
month_lt
Date month is less than.
month_le
Date month is less than or equal to.
day
Date day is equal to.
day_ne
Date day is not equal to.
day_gt
Date day is greater than.
day_ge
Date day is greater than or equal to.
day_lt
Date day is less than.
day_le
Date day is less than or equal to.
Methods
filter_expr
Transform Django-style filters into SQLAlchemy expressions.
Takes keyword arguments like:
and returns list of expressions like:About alias
When using alias, for example:
the query cannot be executed like
because it will be compiled to
which is wrong. The select is made from
post_1
but filter is based onpost
. Such filter will not work.A correct way to execute such query is
For such case, this method (and other methods like
order_expr()
andcolumns_expr()
) can be called ON ALIAS:Note
This is a very low-level method. It is intended for more flexibility. It does not do magic Django-like joins. Use the high-level
smart_query()
method for that.Parameters
filters
: Django-style filters.Returns
list[sqlalchemy.sql.elements.ColumnElement[Any]]
: Filter expressions.Raises
OperatorError
: If operator is not found.NoFilterableError
: If attribute is not filterable.Examples
Usage:
>>> Post.filter_expr(rating=5) [Post.rating == 5] >>> db.query(Post).filter(*Post.filter_expr(rating=5)) SELECT * FROM posts WHERE post.rating=5 >>> Post.filter_expr(rating=5, user_id__in=[1,2]) [Post.rating == 5, Post.user_id.in_([1,2])] >>> db.query(Post).filter( ... *Post.filter_expr(rating=5, user_id__in=[1,2]) ... ) SELECT * FROM posts WHERE post.rating=5 AND post.user_id IN [1, 2]
Using alias:
>>> alias = aliased(Post) >>> alias.filter_expr(rating=5) [Post.rating == 5] >>> db.query(alias).filter(*alias.filter_expr(rating=5)) SELECT * FROM post_1 WHERE post_1.rating=5 >>> alias.filter_expr(rating=5, user_id__in=[1,2]) [Post.rating == 5, Post.user_id.in_([1,2])] >>> db.query(alias).filter( ... *alias.filter_expr(rating=5, user_id__in=[1,2]) ... ) SELECT * FROM post_1 WHERE post_1.rating=5 AND post_1.user_id IN [1, 2]
order_expr
Transforms Django-style order expressions into SQLAlchemy expressions.
Takes list of columns to order by like:
and returns list of expressions like:About alias
See the
filter_expr()
method documentation for more information about using alias.Note
This is a very low-level method. It is intended for more flexibility. It does not do magic Django-like joins. Use the high-level
smart_query()
method for that.Parameters
columns
: Django-style sort expressions.Returns
list[sqlalchemy.sql.elements.ColumnElement[Any]]
: Sort expressions.Raises
NoSortableError
: If attribute is not sortable.Examples
Usage:
>>> Post.order_expr('-rating') [desc(Post.rating)] >>> db.query(Post).order_by(*Post.order_expr('-rating')) SELECT * FROM posts ORDER BY posts.rating DESC >>> Post.order_expr('-rating', 'title') [desc(Post.rating), asc(Post.title)] >>> db.query(Post).order_by( ... *Post.order_expr('-rating', 'title') ... ) SELECT * FROM posts ORDER BY posts.rating DESC, posts.title ASC
Using alias:
>>> alias = aliased(Post) >>> alias.order_expr('-rating') [desc(Post.rating)] >>> db.query(alias).order_by(*alias.order_expr('-rating')) SELECT * FROM posts_1 ORDER BY posts_1.rating DESC >>> alias.order_expr('-rating', 'title') [desc(Post.rating), asc(Post.title)] >>> db.query(alias).order_by(*alias.order_expr('-rating', 'title')) SELECT * FROM posts_1 ORDER BY posts_1.rating DESC, posts_1.title ASC
columns_expr
Transforms column names into SQLAlchemy model attributes.
Takes list of column names like:
and returns list of model attributes like: This method mostly used for grouping.About alias
See the
filter_expr()
method documentation for more information about using alias.Note
This is a very low-level method. It is intended for more flexibility. It does not do magic Django-like joins. Use the high-level
smart_query()
method for that.Parameters
columns
: Column names.Returns
list[sqlalchemy.sql.elements.ColumnElement[Any]]
: Model attributes.Raises
NoColumnOrHybridPropertyError
: If attribute is neither a column nor a hybrid property.Examples
Usage:
>>> Post.columns_expr('user_id') [Post.user_id] >>> Post.columns_expr('user_id', 'rating') [Post.user_id, Post.rating]
Grouping:
>>> from sqlalchemy.sql import func >>> db.query(Post.user_id, func.max(Post.rating)) ... .group_by(*Post.columns_expr('user_id')) SELECT posts.user_id, max(posts.rating) FROM posts GROUP BY posts.user_id >>> db.query(Post.user_id, Post.rating) ... .group_by(*Post.columns_expr('user_id', 'rating')) SELECT posts.user_id, posts.rating FROM posts GROUP BY posts.user_id, posts.rating
Using alias:
>>> alias = aliased(Post) >>> alias.columns_expr('user_id') [Post.user_id] >>> alias.columns_expr('user_id', 'rating') [Post.user_id, Post.rating]
Grouping on alias:
>>> db.query(alias.user_id, func.max(alias.rating)) ... .group_by(*alias.columns_expr('user_id')) SELECT posts_1.user_id FROM posts_1 GROUP BY posts_1.user_id >>> db.query(alias.user_id, alias.rating) ... .group_by(*alias.columns_expr('user_id', 'rating')) SELECT posts_1.user_id, posts_1.rating FROM posts_1 GROUP BY posts_1.user_id, posts_1.rating
eager_expr
Transforms an eager loading defined schema into SQLAlchemy eager loading expressions.
Takes a schema like:
and returns eager loading expressions like:schema = { Post.user: 'joined', # joinedload user Post.comments: ('subquery', { # load comments in separate query Comment.user: 'joined' # but, in this separate query, join user }) }
The supported eager loading strategies are: -
'joined'
:sqlalchemy.orm.joinedload()
-'subquery'
:sqlalchemy.orm.subqueryload()
-'selectin'
:sqlalchemy.orm.selectinload()
The constants
JOINED
,SUBQUERY
andSELECT_IN
are defined in thesqlactive.definitions
module and can be used instead of the strings:>>> from sqlactive.definitions import JOINED, SUBQUERY >>> schema = { ... Post.user: JOINED, ... Post.comments: (SUBQUERY, { ... Comment.user: JOINED ... }) ... }
Parameters
schema
: Schema for the eager loading.Returns
list[sqlalchemy.orm.strategy_options._AbstractLoad]
: Eager loading expressions.Examples
smart_query
@classmethod
def smart_query(
cls,
query: Query,
criteria: Sequence[_ColumnExpressionArgument[bool]] | None = None,
filters: DjangoFilters | None = None,
sort_columns: Sequence[_ColumnExpressionOrStrLabelArgument[Any]] | None = None,
sort_attrs: Sequence[str] | None = None,
group_columns: Sequence[_ColumnExpressionOrStrLabelArgument[Any]] | None = None,
group_attrs: Sequence[str] | None = None,
schema: EagerSchema | None = None,
) -> Query:
Creates a query combining filtering, sorting, grouping and eager loading.
Does magic
Django-like joins
like:Does filtering, sorting, grouping and eager loading at the same time. And if, say, filters, sorting and grouping need the same join, it will be done only once.
It also supports SQLAlchemy syntax like:
>>> db.query(User).filter(User.id == 1, User.name == 'Bob') >>> db.query(User).filter(or_(User.id == 1, User.name == 'Bob')) >>> db.query(Post).order_by(Post.rating.desc()) >>> db.query(Post).order_by(desc(Post.rating), asc(Post.user_id))
Note
For more flexibility, you can use the
filter_expr()
,order_expr()
,columns_expr()
andeager_expr()
methods.Parameters
query
: Native SQLAlchemy query.criteria
: SQLAlchemy syntax filter expressions.filters
: Django-like filter expressions.sort_columns
: Standalone sort columns.sort_attrs
: Django-like sort expressions.group_columns
: Standalone group columns.group_attrs
: Django-like group expressions.schema
: Schema for the eager loading.Returns
Query
: SQLAlchemy query with filtering, sorting, grouping and eager loading, that is to say, a beautiful smart query.Examples
>>> query = User.smart_query( ... criteria=(or_(User.age == 30, User.age == 32),), ... filters={'username__like': '%8'}, ... sort_columns=(User.username,), ... sort_attrs=('age',), ... schema={ ... User.posts: JOINED, ... User.comments: (SUBQUERY, { ... Comment.post: SELECT_IN ... }) ... }, ... ) >>> users = await query.unique_all() >>> [user.username for user in users] ['Bob28', 'Ian48', 'Jessica3248'] >>> users[0].posts[0].title Lorem ipsum >>> users[0].comments[0].post.title Lorem ipsum
apply_search_filter
@classmethod
def apply_search_filter(
query: Query,
search_term: str,
columns: Sequence[str | InstrumentedAttribute[Any]] | None = None,
) -> Query
Applies a search filter to the query.
Searches for
search_term
in the searchable columns of the model. Ifcolumns
are provided, searches only these columns.Parameters
query
: Native SQLAlchemy query.search_term
: Search term.columns
: Columns to search in.Returns
Query
: SQLAlchemy query with the search filter applied.Examples
To learn how to use this method, see the search()
method. It uses this method internally.