Add NULLS ordering
issue5858 review27651002
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
* Add NULLS ordering
|
||||
* Add context domain on ir.action.act_window
|
||||
* Allow None limit in action window
|
||||
* Add has_window_functions on Database
|
||||
|
||||
@@ -378,7 +378,9 @@ Class attributes are:
|
||||
|
||||
where the first element of the tuple is a field name of the model and the
|
||||
second is the sort ordering as `ASC` for ascending or `DESC` for
|
||||
descending.
|
||||
descending. This second element may contain 'NULLS FIRST' or 'NULLS LAST'
|
||||
to sort null values before or after non-null values. If neither is
|
||||
specified the default behavior of the backend is used.
|
||||
|
||||
In case the field used for the first element is a :class:`fields.Many2One`,
|
||||
it is also possible to use the dotted notation to sort on a specific field
|
||||
@@ -687,14 +689,13 @@ Class methods:
|
||||
sequence_ordered
|
||||
================
|
||||
|
||||
.. method:: sequence_ordered([field_name, [field_label, [order, [null_first]]]])
|
||||
.. method:: sequence_ordered([field_name, [field_label, [order]]])
|
||||
|
||||
Retuns a mixin_ class which defines the order of a :class:`ModelSQL` with an
|
||||
:class:`trytond.model.fields.Integer` field. field_name indicates the name of
|
||||
the field to be created and its default values is `sequence`. field_label
|
||||
defines the label which will be used by the field and defaults to `Sequence`.
|
||||
Order specifies the order direction and defaults to `ASC` and null first
|
||||
if the null values should be ordered first and defaults to `True`.
|
||||
Order specifies the order direction and defaults to `ASC NULLS FIRST`.
|
||||
|
||||
|
||||
.. _mixin: http://en.wikipedia.org/wiki/Mixin
|
||||
|
||||
@@ -75,7 +75,9 @@ class Database(DatabaseInterface):
|
||||
|
||||
_list_cache = None
|
||||
_list_cache_timestamp = None
|
||||
flavor = Flavor(max_limit=18446744073709551610, function_mapping=MAPPING)
|
||||
flavor = Flavor(
|
||||
max_limit=18446744073709551610, function_mapping=MAPPING,
|
||||
null_ordering=False)
|
||||
|
||||
def connect(self):
|
||||
return self
|
||||
|
||||
@@ -221,7 +221,8 @@ class Database(DatabaseInterface):
|
||||
|
||||
_local = threading.local()
|
||||
_conn = None
|
||||
flavor = Flavor(paramstyle='qmark', function_mapping=MAPPING)
|
||||
flavor = Flavor(
|
||||
paramstyle='qmark', function_mapping=MAPPING, null_ordering=False)
|
||||
IN_MAX = 200
|
||||
|
||||
def __new__(cls, name=':memory:'):
|
||||
|
||||
@@ -4,7 +4,8 @@ import datetime
|
||||
from itertools import islice, izip, chain, ifilter
|
||||
from collections import OrderedDict
|
||||
|
||||
from sql import Table, Column, Literal, Desc, Asc, Expression, Null
|
||||
from sql import (Table, Column, Literal, Desc, Asc, Expression, Null,
|
||||
NullsFirst, NullsLast)
|
||||
from sql.functions import CurrentTimestamp, Extract
|
||||
from sql.conditionals import Coalesce
|
||||
from sql.operators import Or, And, Operator
|
||||
@@ -1149,14 +1150,25 @@ class ModelSQL(ModelStorage):
|
||||
'DESC': Desc,
|
||||
'ASC': Asc,
|
||||
}
|
||||
null_ordering_types = {
|
||||
'NULLS FIRST': NullsFirst,
|
||||
'NULLS LAST': NullsLast,
|
||||
None: lambda _: _
|
||||
}
|
||||
if order is None or order is False:
|
||||
order = cls._order
|
||||
for oexpr, otype in order:
|
||||
fname, _, extra_expr = oexpr.partition('.')
|
||||
field = cls._fields[fname]
|
||||
Order = order_types[otype.upper()]
|
||||
otype = otype.upper()
|
||||
try:
|
||||
otype, null_ordering = otype.split(' ', 1)
|
||||
except ValueError:
|
||||
null_ordering = None
|
||||
Order = order_types[otype]
|
||||
NullOrdering = null_ordering_types[null_ordering]
|
||||
forder = field.convert_order(oexpr, tables, cls)
|
||||
order_by.extend((Order(o) for o in forder))
|
||||
order_by.extend((NullOrdering(Order(o)) for o in forder))
|
||||
|
||||
# construct a clause for the rules :
|
||||
domain = Rule.domain_get(cls.__name__, mode='read')
|
||||
|
||||
+1
-15
@@ -1,14 +1,10 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the toplevel of this
|
||||
# repository contains the full copyright notices and license terms.
|
||||
from sql import Null, Column
|
||||
from sql.operators import Equal, NotEqual
|
||||
from sql.conditionals import Case
|
||||
|
||||
from trytond.model import fields
|
||||
|
||||
|
||||
def sequence_ordered(field_name='sequence', field_label='Sequence',
|
||||
order='ASC', null_first=True):
|
||||
order='ASC NULLS FIRST'):
|
||||
"Returns a mixin to order the model by order fields"
|
||||
|
||||
class SequenceOrderedMixin(object):
|
||||
@@ -20,14 +16,4 @@ def sequence_ordered(field_name='sequence', field_label='Sequence',
|
||||
cls._order = [(field_name, order)] + cls._order
|
||||
|
||||
setattr(SequenceOrderedMixin, field_name, fields.Integer(field_label))
|
||||
|
||||
@classmethod
|
||||
def order_function(cls, tables):
|
||||
table, _ = tables[None]
|
||||
operator = Equal
|
||||
if not null_first:
|
||||
operator = NotEqual
|
||||
field = Column(table, field_name)
|
||||
return [Case((operator(field, Null), 0), else_=1), field]
|
||||
setattr(SequenceOrderedMixin, 'order_%s' % field_name, order_function)
|
||||
return SequenceOrderedMixin
|
||||
|
||||
@@ -178,6 +178,7 @@ def register():
|
||||
TestHistoryLine,
|
||||
FieldContextChild,
|
||||
FieldContextParent,
|
||||
NullOrder,
|
||||
module='tests', type_='model')
|
||||
Pool.register(
|
||||
TestWizard,
|
||||
|
||||
@@ -13,6 +13,7 @@ __all__ = [
|
||||
'Union', 'UnionUnion',
|
||||
'Model4UnionTree1', 'Model4UnionTree2', 'UnionTree',
|
||||
'SequenceOrderedModel',
|
||||
'NullOrder',
|
||||
]
|
||||
|
||||
|
||||
@@ -161,3 +162,9 @@ class UnionTree(UnionMixin, ModelSQL):
|
||||
class SequenceOrderedModel(sequence_ordered(), ModelSQL):
|
||||
'Sequence Ordered Model'
|
||||
__name__ = 'test.order.sequence'
|
||||
|
||||
|
||||
class NullOrder(ModelSQL):
|
||||
"Null Order"
|
||||
__name__ = 'test.null_order'
|
||||
integer = fields.Integer('Integer')
|
||||
|
||||
@@ -133,6 +133,32 @@ class ModelSQLTestCase(unittest.TestCase):
|
||||
TargetModel.name.string, TargetModel.__doc__)
|
||||
self.assertEqual(err.message, msg)
|
||||
|
||||
@with_transaction()
|
||||
def test_null_ordering(self):
|
||||
'Test NULL ordering'
|
||||
pool = Pool()
|
||||
NullOrder = pool.get('test.null_order')
|
||||
|
||||
NullOrder.create([{
|
||||
'integer': 1,
|
||||
}, {
|
||||
'integer': 3,
|
||||
}, {
|
||||
'integer': None,
|
||||
}])
|
||||
integers = NullOrder.search([], order=[('integer', 'ASC NULLS FIRST')])
|
||||
self.assertListEqual([i.integer for i in integers], [None, 1, 3])
|
||||
|
||||
integers = NullOrder.search(
|
||||
[], order=[('integer', 'DESC NULLS FIRST')])
|
||||
self.assertListEqual([i.integer for i in integers], [None, 3, 1])
|
||||
|
||||
integers = NullOrder.search([], order=[('integer', 'ASC NULLS LAST')])
|
||||
self.assertListEqual([i.integer for i in integers], [1, 3, None])
|
||||
|
||||
integers = NullOrder.search([], order=[('integer', 'DESC NULLS LAST')])
|
||||
self.assertListEqual([i.integer for i in integers], [3, 1, None])
|
||||
|
||||
|
||||
def suite():
|
||||
return unittest.TestLoader().loadTestsFromTestCase(ModelSQLTestCase)
|
||||
|
||||
Reference in New Issue
Block a user