92c5db2eaa54e59aa1dc20b10d0116dbc947c983
[openstack-build/neutron-build.git] / neutron / db / sqlalchemyutils.py
1 # Copyright 2011 OpenStack Foundation.
2 # All Rights Reserved.
3 #
4 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
5 #    not use this file except in compliance with the License. You may obtain
6 #    a copy of the License at
7 #
8 #         http://www.apache.org/licenses/LICENSE-2.0
9 #
10 #    Unless required by applicable law or agreed to in writing, software
11 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 #    License for the specific language governing permissions and limitations
14 #    under the License.
15
16 from six import moves
17 import sqlalchemy
18 from sqlalchemy.orm import properties
19
20 from neutron._i18n import _
21 from neutron.common import exceptions as n_exc
22
23
24 def paginate_query(query, model, limit, sorts, marker_obj=None):
25     """Returns a query with sorting / pagination criteria added.
26
27     Pagination works by requiring a unique sort key, specified by sorts.
28     (If sort keys is not unique, then we risk looping through values.)
29     We use the last row in the previous page as the 'marker' for pagination.
30     So we must return values that follow the passed marker in the order.
31     With a single-valued sort key, this would be easy: sort_key > X.
32     With a compound-values sort key, (k1, k2, k3) we must do this to repeat
33     the lexicographical ordering:
34     (k1 > X1) or (k1 == X1 && k2 > X2) or (k1 == X1 && k2 == X2 && k3 > X3)
35     The reason of didn't use OFFSET clause was it don't scale, please refer
36     discussion at https://lists.launchpad.net/openstack/msg02547.html
37
38     We also have to cope with different sort directions.
39
40     Typically, the id of the last row is used as the client-facing pagination
41     marker, then the actual marker object must be fetched from the db and
42     passed in to us as marker.
43
44     :param query: the query object to which we should add paging/sorting
45     :param model: the ORM model class
46     :param limit: maximum number of items to return
47     :param sorts: array of attributes and direction by which results should
48                  be sorted
49     :param marker: the last item of the previous page; we returns the next
50                     results after this value.
51     :rtype: sqlalchemy.orm.query.Query
52     :return: The query with sorting/pagination added.
53     """
54     if not sorts:
55         return query
56
57     # A primary key must be specified in sort keys
58     assert not (limit and
59                 len(set(dict(sorts).keys()) &
60                     set(model.__table__.primary_key.columns.keys())) == 0)
61
62     # Add sorting
63     for sort_key, sort_direction in sorts:
64         sort_dir_func = sqlalchemy.asc if sort_direction else sqlalchemy.desc
65         try:
66             sort_key_attr = getattr(model, sort_key)
67         except AttributeError:
68             # Extension attribute doesn't support for sorting. Because it
69             # existed in attr_info, it will be caught here
70             msg = _("%s is invalid attribute for sort_key") % sort_key
71             raise n_exc.BadRequest(resource=model.__tablename__, msg=msg)
72         if isinstance(sort_key_attr.property, properties.RelationshipProperty):
73             msg = _("The attribute '%(attr)s' is reference to other "
74                     "resource, can't used by sort "
75                     "'%(resource)s'") % {'attr': sort_key,
76                                          'resource': model.__tablename__}
77             raise n_exc.BadRequest(resource=model.__tablename__, msg=msg)
78         query = query.order_by(sort_dir_func(sort_key_attr))
79
80     # Add pagination
81     if marker_obj:
82         marker_values = [getattr(marker_obj, sort[0]) for sort in sorts]
83
84         # Build up an array of sort criteria as in the docstring
85         criteria_list = []
86         for i, sort in enumerate(sorts):
87             crit_attrs = [(getattr(model, sorts[j][0]) == marker_values[j])
88                           for j in moves.range(i)]
89             model_attr = getattr(model, sort[0])
90             if sort[1]:
91                 crit_attrs.append((model_attr > marker_values[i]))
92             else:
93                 crit_attrs.append((model_attr < marker_values[i]))
94
95             criteria = sqlalchemy.sql.and_(*crit_attrs)
96             criteria_list.append(criteria)
97
98         f = sqlalchemy.sql.or_(*criteria_list)
99         query = query.filter(f)
100
101     if limit:
102         query = query.limit(limit)
103
104     return query