Add make_order_by_deterministic

This commit is contained in:
Konsta Vesterinen
2014-10-20 17:54:10 +03:00
parent 174ca93101
commit b5afffd5c3
9 changed files with 157 additions and 15 deletions

View File

@@ -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)

View File

@@ -76,6 +76,12 @@ identity
.. autofunction:: identity
make_order_by_deterministic
^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autofunction:: make_order_by_deterministic
naturally_equivalent
^^^^^^^^^^^^^^^^^^^^

View File

@@ -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',

View File

@@ -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

View File

@@ -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):

View File

@@ -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

View File

@@ -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__
]

View 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)

View File

@@ -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, '')