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 support for more SQLAlchemy based objects and classes in get_tables function
|
||||||
- Added has_unique_index utility function
|
- Added has_unique_index utility function
|
||||||
|
- Added make_order_by_deterministic utility function
|
||||||
|
|
||||||
|
|
||||||
0.27.0 (2014-10-14)
|
0.27.0 (2014-10-14)
|
||||||
|
@@ -76,6 +76,12 @@ identity
|
|||||||
.. autofunction:: identity
|
.. autofunction:: identity
|
||||||
|
|
||||||
|
|
||||||
|
make_order_by_deterministic
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. autofunction:: make_order_by_deterministic
|
||||||
|
|
||||||
|
|
||||||
naturally_equivalent
|
naturally_equivalent
|
||||||
^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
@@ -1,7 +1,11 @@
|
|||||||
from .defer_except import defer_except
|
from .defer_except import defer_except
|
||||||
from .mock import create_mock_engine, mock_engine
|
from .mock import create_mock_engine, mock_engine
|
||||||
from .render import render_expression, render_statement
|
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 (
|
from .database import (
|
||||||
analyze,
|
analyze,
|
||||||
create_database,
|
create_database,
|
||||||
@@ -63,6 +67,7 @@ __all__ = (
|
|||||||
'identity',
|
'identity',
|
||||||
'is_auto_assigned_date_column',
|
'is_auto_assigned_date_column',
|
||||||
'is_indexed_foreign_key',
|
'is_indexed_foreign_key',
|
||||||
|
'make_order_by_deterministic',
|
||||||
'mock_engine',
|
'mock_engine',
|
||||||
'naturally_equivalent',
|
'naturally_equivalent',
|
||||||
'non_indexed_foreign_keys',
|
'non_indexed_foreign_keys',
|
||||||
|
@@ -204,7 +204,7 @@ def has_unique_index(column):
|
|||||||
(column is pks.values()[0] and len(pks) == 1)
|
(column is pks.values()[0] and len(pks) == 1)
|
||||||
or
|
or
|
||||||
any(
|
any(
|
||||||
constraint.columns.values()[0] is column and
|
match_columns(constraint.columns.values()[0], column) and
|
||||||
len(constraint.columns) == 1
|
len(constraint.columns) == 1
|
||||||
for constraint in column.table.constraints
|
for constraint in column.table.constraints
|
||||||
if isinstance(constraint, sa.sql.schema.UniqueConstraint)
|
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):
|
def is_auto_assigned_date_column(column):
|
||||||
"""
|
"""
|
||||||
Returns whether or not given SQLAlchemy Column object's is auto assigned
|
Returns whether or not given SQLAlchemy Column object's is auto assigned
|
||||||
|
@@ -83,6 +83,8 @@ def get_mapper(mixed):
|
|||||||
mixed = mixed.element
|
mixed = mixed.element
|
||||||
if isinstance(mixed, AliasedInsp):
|
if isinstance(mixed, AliasedInsp):
|
||||||
return mixed.mapper
|
return mixed.mapper
|
||||||
|
if isinstance(mixed, sa.orm.query._MapperEntity):
|
||||||
|
mixed = mixed.expr
|
||||||
if isinstance(mixed, sa.orm.attributes.InstrumentedAttribute):
|
if isinstance(mixed, sa.orm.attributes.InstrumentedAttribute):
|
||||||
mixed = mixed.class_
|
mixed = mixed.class_
|
||||||
if isinstance(mixed, sa.Table):
|
if isinstance(mixed, sa.Table):
|
||||||
@@ -205,8 +207,8 @@ def get_tables(mixed):
|
|||||||
.. versionadded: 0.26.0
|
.. versionadded: 0.26.0
|
||||||
|
|
||||||
:param mixed:
|
:param mixed:
|
||||||
SQLAlchemy Mapper / Declarative class or a SA Alias object wrapping
|
SQLAlchemy Mapper, Declarative class, Column, InstrumentedAttribute or
|
||||||
any of these objects.
|
a SA Alias object wrapping any of these objects.
|
||||||
"""
|
"""
|
||||||
if isinstance(mixed, sa.Table):
|
if isinstance(mixed, sa.Table):
|
||||||
return [mixed]
|
return [mixed]
|
||||||
@@ -218,8 +220,12 @@ def get_tables(mixed):
|
|||||||
|
|
||||||
polymorphic_mappers = get_polymorphic_mappers(mapper)
|
polymorphic_mappers = get_polymorphic_mappers(mapper)
|
||||||
if polymorphic_mappers:
|
if polymorphic_mappers:
|
||||||
return sum((m.tables for m in polymorphic_mappers), [])
|
tables = sum((m.tables for m in polymorphic_mappers), [])
|
||||||
return mapper.tables
|
tables = mapper.tables
|
||||||
|
|
||||||
|
if isinstance(mixed, sa.orm.attributes.InstrumentedAttribute):
|
||||||
|
mixed = mixed.class_
|
||||||
|
return tables
|
||||||
|
|
||||||
|
|
||||||
def get_columns(mixed):
|
def get_columns(mixed):
|
||||||
|
@@ -1,5 +1,8 @@
|
|||||||
|
import sqlalchemy as sa
|
||||||
from sqlalchemy.sql.expression import desc, asc
|
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):
|
class QuerySorterException(Exception):
|
||||||
@@ -130,3 +133,60 @@ def sort_query(query, *args, **kwargs):
|
|||||||
be raised for unknown columns.
|
be raised for unknown columns.
|
||||||
"""
|
"""
|
||||||
return QuerySorter(**kwargs)(query, *args)
|
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__
|
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):
|
def test_column(self):
|
||||||
assert get_tables(self.Article.__table__.c.id) == [
|
assert get_tables(self.Article.__table__.c.id) == [
|
||||||
self.Article.__table__
|
self.Article.__table__
|
||||||
@@ -53,3 +64,8 @@ class TestGetTables(TestCase):
|
|||||||
self.TextItem.__table__, self.Article.__table__
|
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
|
import sqlalchemy as sa
|
||||||
from sqlalchemy_utils import sort_query
|
from sqlalchemy_utils import sort_query
|
||||||
from sqlalchemy_utils.functions import QuerySorterException
|
from sqlalchemy_utils.functions import QuerySorterException
|
||||||
from tests import TestCase
|
from tests import assert_contains, TestCase
|
||||||
|
|
||||||
|
|
||||||
def assert_contains(clause, query):
|
|
||||||
# Test that query executes
|
|
||||||
query.all()
|
|
||||||
assert clause in str(query)
|
|
||||||
|
|
||||||
|
|
||||||
class TestSortQuery(TestCase):
|
class TestSortQuery(TestCase):
|
||||||
|
|
||||||
def test_without_sort_param_returns_the_query_object_untouched(self):
|
def test_without_sort_param_returns_the_query_object_untouched(self):
|
||||||
query = self.session.query(self.Article)
|
query = self.session.query(self.Article)
|
||||||
query = sort_query(query, '')
|
query = sort_query(query, '')
|
||||||
|
Reference in New Issue
Block a user