Add make_order_by_deterministic
This commit is contained in:
@@ -9,6 +9,7 @@ Here you can see the full list of changes between each SQLAlchemy-Utils release.
|
||||
|
||||
- Added support for more SQLAlchemy based objects and classes in get_tables function
|
||||
- Added has_unique_index utility function
|
||||
- Added make_order_by_deterministic utility function
|
||||
|
||||
|
||||
0.27.0 (2014-10-14)
|
||||
|
@@ -76,6 +76,12 @@ identity
|
||||
.. autofunction:: identity
|
||||
|
||||
|
||||
make_order_by_deterministic
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autofunction:: make_order_by_deterministic
|
||||
|
||||
|
||||
naturally_equivalent
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
@@ -1,7 +1,11 @@
|
||||
from .defer_except import defer_except
|
||||
from .mock import create_mock_engine, mock_engine
|
||||
from .render import render_expression, render_statement
|
||||
from .sort_query import sort_query, QuerySorterException
|
||||
from .sort_query import (
|
||||
make_order_by_deterministic,
|
||||
sort_query,
|
||||
QuerySorterException
|
||||
)
|
||||
from .database import (
|
||||
analyze,
|
||||
create_database,
|
||||
@@ -63,6 +67,7 @@ __all__ = (
|
||||
'identity',
|
||||
'is_auto_assigned_date_column',
|
||||
'is_indexed_foreign_key',
|
||||
'make_order_by_deterministic',
|
||||
'mock_engine',
|
||||
'naturally_equivalent',
|
||||
'non_indexed_foreign_keys',
|
||||
|
@@ -204,7 +204,7 @@ def has_unique_index(column):
|
||||
(column is pks.values()[0] and len(pks) == 1)
|
||||
or
|
||||
any(
|
||||
constraint.columns.values()[0] is column and
|
||||
match_columns(constraint.columns.values()[0], column) and
|
||||
len(constraint.columns) == 1
|
||||
for constraint in column.table.constraints
|
||||
if isinstance(constraint, sa.sql.schema.UniqueConstraint)
|
||||
@@ -212,6 +212,10 @@ def has_unique_index(column):
|
||||
)
|
||||
|
||||
|
||||
def match_columns(column, column2):
|
||||
return column.table is column2.table and column.name == column2.name
|
||||
|
||||
|
||||
def is_auto_assigned_date_column(column):
|
||||
"""
|
||||
Returns whether or not given SQLAlchemy Column object's is auto assigned
|
||||
|
@@ -83,6 +83,8 @@ def get_mapper(mixed):
|
||||
mixed = mixed.element
|
||||
if isinstance(mixed, AliasedInsp):
|
||||
return mixed.mapper
|
||||
if isinstance(mixed, sa.orm.query._MapperEntity):
|
||||
mixed = mixed.expr
|
||||
if isinstance(mixed, sa.orm.attributes.InstrumentedAttribute):
|
||||
mixed = mixed.class_
|
||||
if isinstance(mixed, sa.Table):
|
||||
@@ -205,8 +207,8 @@ def get_tables(mixed):
|
||||
.. versionadded: 0.26.0
|
||||
|
||||
:param mixed:
|
||||
SQLAlchemy Mapper / Declarative class or a SA Alias object wrapping
|
||||
any of these objects.
|
||||
SQLAlchemy Mapper, Declarative class, Column, InstrumentedAttribute or
|
||||
a SA Alias object wrapping any of these objects.
|
||||
"""
|
||||
if isinstance(mixed, sa.Table):
|
||||
return [mixed]
|
||||
@@ -218,8 +220,12 @@ def get_tables(mixed):
|
||||
|
||||
polymorphic_mappers = get_polymorphic_mappers(mapper)
|
||||
if polymorphic_mappers:
|
||||
return sum((m.tables for m in polymorphic_mappers), [])
|
||||
return mapper.tables
|
||||
tables = sum((m.tables for m in polymorphic_mappers), [])
|
||||
tables = mapper.tables
|
||||
|
||||
if isinstance(mixed, sa.orm.attributes.InstrumentedAttribute):
|
||||
mixed = mixed.class_
|
||||
return tables
|
||||
|
||||
|
||||
def get_columns(mixed):
|
||||
|
@@ -1,5 +1,8 @@
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.sql.expression import desc, asc
|
||||
from .orm import get_query_descriptor
|
||||
|
||||
from .database import has_unique_index
|
||||
from .orm import get_query_descriptor, get_tables
|
||||
|
||||
|
||||
class QuerySorterException(Exception):
|
||||
@@ -130,3 +133,60 @@ def sort_query(query, *args, **kwargs):
|
||||
be raised for unknown columns.
|
||||
"""
|
||||
return QuerySorter(**kwargs)(query, *args)
|
||||
|
||||
|
||||
def make_order_by_deterministic(query):
|
||||
"""
|
||||
Make query order by deterministic (if it isn't already). Order by is
|
||||
considered deterministic if it contains column that is unique index (
|
||||
either it is a primary key or has a unique index). Many times it is design
|
||||
flaw to order by queries in nondeterministic manner.
|
||||
|
||||
Consider a User model with three fields: id (primary key), favorite color
|
||||
and email (unique).::
|
||||
|
||||
|
||||
from sqlalchemy_utils import make_order_by_deterministic
|
||||
|
||||
|
||||
query = session.query(User).order_by(User.favorite_color)
|
||||
|
||||
query = make_order_by_deterministic(query)
|
||||
print query # 'SELECT ... ORDER BY "user".favorite_color, "user".id'
|
||||
|
||||
|
||||
query = session.query(User).order_by(User.email)
|
||||
|
||||
query = make_order_by_deterministic(query)
|
||||
print query # 'SELECT ... ORDER BY "user".email'
|
||||
|
||||
|
||||
query = session.query(User).order_by(User.id)
|
||||
|
||||
query = make_order_by_deterministic(query)
|
||||
print query # 'SELECT ... ORDER BY "user".id'
|
||||
|
||||
|
||||
.. versionadded: 0.27.1
|
||||
"""
|
||||
order_by = query._order_by[0]
|
||||
if isinstance(order_by, sa.Column):
|
||||
order_by_func = sa.asc
|
||||
column = order_by
|
||||
elif isinstance(order_by, sa.sql.expression.UnaryExpression):
|
||||
if order_by.modifier == sa.sql.operators.desc_op:
|
||||
order_by_func = sa.desc
|
||||
else:
|
||||
order_by_func = sa.asc
|
||||
column = order_by.get_children()[0]
|
||||
else:
|
||||
raise TypeError('Only simple columns in query order by are supported.')
|
||||
|
||||
if has_unique_index(column):
|
||||
return query
|
||||
|
||||
base_table = get_tables(query._entities[0])[0]
|
||||
query = query.order_by(
|
||||
*(order_by_func(c) for c in base_table.c if c.primary_key)
|
||||
)
|
||||
return query
|
||||
|
@@ -42,6 +42,17 @@ class TestGetTables(TestCase):
|
||||
self.Article.__table__
|
||||
]
|
||||
|
||||
def test_instrumented_attribute(self):
|
||||
assert get_tables(self.TextItem.name) == [
|
||||
self.TextItem.__table__,
|
||||
]
|
||||
|
||||
def test_polymorphic_instrumented_attribute(self):
|
||||
assert get_tables(self.Article.id) == [
|
||||
self.TextItem.__table__,
|
||||
self.Article.__table__
|
||||
]
|
||||
|
||||
def test_column(self):
|
||||
assert get_tables(self.Article.__table__.c.id) == [
|
||||
self.Article.__table__
|
||||
@@ -53,3 +64,8 @@ class TestGetTables(TestCase):
|
||||
self.TextItem.__table__, self.Article.__table__
|
||||
]
|
||||
|
||||
def test_mapper_entity(self):
|
||||
query = self.session.query(self.Article)
|
||||
assert get_tables(query._entities[0]) == [
|
||||
self.TextItem.__table__, self.Article.__table__
|
||||
]
|
||||
|
51
tests/functions/test_make_order_by_deterministic.py
Normal file
51
tests/functions/test_make_order_by_deterministic.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from pytest import raises
|
||||
import sqlalchemy as sa
|
||||
|
||||
from sqlalchemy_utils.functions.sort_query import make_order_by_deterministic
|
||||
|
||||
from tests import assert_contains, TestCase
|
||||
|
||||
|
||||
class TestMakeOrderByDeterministic(TestCase):
|
||||
def create_models(self):
|
||||
class User(self.Base):
|
||||
__tablename__ = 'user'
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
name = sa.Column(sa.Unicode)
|
||||
email = sa.Column(sa.Unicode, unique=True)
|
||||
|
||||
email_lower = sa.orm.column_property(
|
||||
sa.func.lower(name)
|
||||
)
|
||||
|
||||
self.User = User
|
||||
|
||||
def test_column_property(self):
|
||||
query = self.session.query(self.User).order_by(self.User.email_lower)
|
||||
with raises(TypeError):
|
||||
make_order_by_deterministic(query)
|
||||
|
||||
def test_unique_column(self):
|
||||
query = self.session.query(self.User).order_by(self.User.email)
|
||||
query = make_order_by_deterministic(query)
|
||||
|
||||
assert str(query).endswith('ORDER BY "user".email')
|
||||
|
||||
def test_non_unique_column(self):
|
||||
query = self.session.query(self.User).order_by(self.User.name)
|
||||
query = make_order_by_deterministic(query)
|
||||
assert_contains('ORDER BY "user".name, "user".id ASC', query)
|
||||
|
||||
def test_descending_order_by(self):
|
||||
query = self.session.query(self.User).order_by(
|
||||
sa.desc(self.User.name)
|
||||
)
|
||||
query = make_order_by_deterministic(query)
|
||||
assert_contains('ORDER BY "user".name DESC, "user".id DESC', query)
|
||||
|
||||
def test_ascending_order_by(self):
|
||||
query = self.session.query(self.User).order_by(
|
||||
sa.asc(self.User.name)
|
||||
)
|
||||
query = make_order_by_deterministic(query)
|
||||
assert_contains('ORDER BY "user".name ASC, "user".id ASC', query)
|
@@ -2,17 +2,10 @@ from pytest import raises
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy_utils import sort_query
|
||||
from sqlalchemy_utils.functions import QuerySorterException
|
||||
from tests import TestCase
|
||||
|
||||
|
||||
def assert_contains(clause, query):
|
||||
# Test that query executes
|
||||
query.all()
|
||||
assert clause in str(query)
|
||||
from tests import assert_contains, TestCase
|
||||
|
||||
|
||||
class TestSortQuery(TestCase):
|
||||
|
||||
def test_without_sort_param_returns_the_query_object_untouched(self):
|
||||
query = self.session.query(self.Article)
|
||||
query = sort_query(query, '')
|
||||
|
Reference in New Issue
Block a user