Skip to content

Commit

Permalink
remove sort_query and get_query_entities
Browse files Browse the repository at this point in the history
The sort_query and get_query_entities functions never worked fully as intended and
contained lots of quirky  edge cases:

1. sort_query function was dangerous in a sense that it could be used for really
inefficient queries (sorting by non-indexed column).
2. The entity string label introspection in both functions relied on SQLAlchemy internals
which were drastically changed in SA 1.4. Relying on those was never a good idea in
the first place.
  • Loading branch information
kvesteri committed Apr 9, 2021
1 parent c1dda36 commit 460e1da
Show file tree
Hide file tree
Showing 6 changed files with 2 additions and 720 deletions.
2 changes: 0 additions & 2 deletions sqlalchemy_utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
get_hybrid_properties,
get_mapper,
get_primary_keys,
get_query_entities,
get_referencing_foreign_keys,
get_tables,
get_type,
Expand All @@ -42,7 +41,6 @@
naturally_equivalent,
render_expression,
render_statement,
sort_query,
table_name
)
from .generic import generic_relationship # noqa
Expand Down
7 changes: 1 addition & 6 deletions sqlalchemy_utils/functions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
get_hybrid_properties,
get_mapper,
get_primary_keys,
get_query_entities,
get_tables,
get_type,
getdotattr,
Expand All @@ -40,8 +39,4 @@
table_name
)
from .render import render_expression, render_statement # noqa
from .sort_query import ( # noqa
make_order_by_deterministic,
QuerySorterException,
sort_query
)
from .sort_query import make_order_by_deterministic # noqa
106 changes: 0 additions & 106 deletions sqlalchemy_utils/functions/orm.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,97 +535,6 @@ def _get_query_compile_state(query):
return query


def query_labels(query):
"""
Return all labels for given SQLAlchemy query object.
Example::
query = session.query(
Category,
db.func.count(Article.id).label('articles')
)
query_labels(query) # ['articles']
:param query: SQLAlchemy Query object
"""
return [
entity._label_name
for entity in _get_query_compile_state(query)._entities
if isinstance(entity, _ColumnEntity) and entity._label_name
]


def get_query_entities(query):
"""
Return a list of all entities present in given SQLAlchemy query object.
Examples::
from sqlalchemy_utils import get_query_entities
query = session.query(Category)
get_query_entities(query) # [<Category>]
query = session.query(Category.id)
get_query_entities(query) # [<Category>]
This function also supports queries with joins.
::
query = session.query(Category).join(Article)
get_query_entities(query) # [<Category>, <Article>]
.. versionchanged: 0.26.7
This function now returns a list instead of generator
:param query: SQLAlchemy Query object
"""
exprs = [
d['expr']
if is_labeled_query(d['expr']) or isinstance(d['expr'], sa.Column)
else d['entity']
for d in query.column_descriptions
]
return [
get_query_entity(expr) for expr in exprs
] + [
get_query_entity(entity)
for entity in _get_query_compile_state(query)._join_entities
]


def is_labeled_query(expr):
return (
isinstance(expr, sa.sql.elements.Label) and
isinstance(
list(expr.base_columns)[0],
(sa.sql.selectable.Select, sa.sql.selectable.ScalarSelect)
)
)


def get_query_entity(expr):
if isinstance(expr, sa.orm.attributes.InstrumentedAttribute):
return expr.parent.class_
elif isinstance(expr, sa.Column):
return expr.table
elif isinstance(expr, AliasedInsp):
return expr.entity
return expr


def get_query_entity_by_alias(query, alias):
entities = get_query_entities(query)

Expand All @@ -649,21 +558,6 @@ def get_polymorphic_mappers(mixed):
return mixed.polymorphic_map.values()


def get_query_descriptor(query, entity, attr):
if attr in query_labels(query):
return attr
else:
entity = get_query_entity_by_alias(query, entity)
if entity:
descriptor = get_descriptor(entity, attr)
if (
hasattr(descriptor, 'property') and
isinstance(descriptor.property, sa.orm.RelationshipProperty)
):
return
return descriptor


def get_descriptor(entity, attr):
mapper = sa.inspect(entity)

Expand Down
132 changes: 1 addition & 131 deletions sqlalchemy_utils/functions/sort_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,137 +2,7 @@
from sqlalchemy.sql.expression import asc, desc

from .database import has_unique_index
from .orm import _get_query_compile_state, get_query_descriptor, get_tables


class QuerySorterException(Exception):
pass


class QuerySorter(object):
def __init__(self, silent=True, separator='-'):
self.separator = separator
self.silent = silent

def assign_order_by(self, entity, attr, func):
expr = get_query_descriptor(self.query, entity, attr)

if expr is not None:
return self.query.order_by(func(expr))
if not self.silent:
raise QuerySorterException(
"Could not sort query with expression '%s'" % attr
)
return self.query

def parse_sort_arg(self, arg):
if arg[0] == self.separator:
func = desc
arg = arg[1:]
else:
func = asc

parts = arg.split(self.separator)
return {
'entity': parts[0] if len(parts) > 1 else None,
'attr': parts[1] if len(parts) > 1 else arg,
'func': func
}

def __call__(self, query, *args):
self.query = query

for sort in args:
if not sort:
continue
self.query = self.assign_order_by(
**self.parse_sort_arg(sort)
)
return self.query


def sort_query(query, *args, **kwargs):
"""
Applies an sql ORDER BY for given query. This function can be easily used
with user-defined sorting.
The examples use the following model definition:
::
import sqlalchemy as sa
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy_utils import sort_query
engine = create_engine(
'sqlite:///'
)
Base = declarative_base()
Session = sessionmaker(bind=engine)
session = Session()
class Category(Base):
__tablename__ = 'category'
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.Unicode(255))
class Article(Base):
__tablename__ = 'article'
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.Unicode(255))
category_id = sa.Column(sa.Integer, sa.ForeignKey(Category.id))
category = sa.orm.relationship(
Category, primaryjoin=category_id == Category.id
)
1. Applying simple ascending sort
::
query = session.query(Article)
query = sort_query(query, 'name')
2. Applying descending sort
::
query = sort_query(query, '-name')
3. Applying sort to custom calculated label
::
query = session.query(
Category, sa.func.count(Article.id).label('articles')
)
query = sort_query(query, 'articles')
4. Applying sort to joined table column
::
query = session.query(Article).join(Article.category)
query = sort_query(query, 'category-name')
:param query:
query to be modified
:param sort:
string that defines the label or column to sort the query by
:param silent:
Whether or not to raise exceptions if unknown sort column
is passed. By default this is `True` indicating that no errors should
be raised for unknown columns.
"""
return QuerySorter(**kwargs)(query, *args)
from .orm import _get_query_compile_state, get_tables


def make_order_by_deterministic(query):
Expand Down
Loading

0 comments on commit 460e1da

Please sign in to comment.