Skip to content

Commit 6e154ff

Browse files
committed
doc: sort the SQLAlchemy example with sort_binding
The example resolved sortBy with Path.resolve, so it needed a request bound to User. With sort_binding, a request bound to a union or to no type sorts the same way. Attributes that users cannot be sorted on are refused.
1 parent adf7512 commit 6e154ff

3 files changed

Lines changed: 33 additions & 18 deletions

File tree

‎doc/integrations/_examples/sqlalchemy_example.py‎

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -300,18 +300,18 @@ def visit_value_path(self, node):
300300

301301

302302
# -- sort-start --
303-
def sort_expression(sort_by, sort_order=None):
304-
"""Return the ``ORDER BY`` term a ``sortBy`` parameter names.
303+
def sort_expression(search_request):
304+
"""Return the ``ORDER BY`` term of the ``sortBy`` of a search request.
305305
306306
A sub-attribute is looked up under the attribute holding it, the way the
307307
filter does: ``meta.lastModified`` is stored in ``("meta", "last_modified")``.
308308
309-
:param sort_by: The ``sortBy`` query parameter.
310-
:param sort_order: The ``sortOrder`` query parameter, ascending by default.
311-
:raises InvalidPathException: If the attribute is unknown, or is stored in
312-
a table an ``ORDER BY`` over users cannot reach.
309+
:param search_request: The query, with its ``sortBy`` and ``sortOrder``.
310+
:raises InvalidPathException: If users cannot be sorted on the attribute,
311+
or if it is stored in a table an ``ORDER BY`` over users cannot reach.
313312
"""
314-
resolved = sort_by.resolve()
313+
sort_by = search_request.sort_by
314+
resolved = search_request.sort_binding(User)
315315
relationship_, column = None, None
316316
if resolved is not None:
317317
relationship_, column = COLUMNS.get(
@@ -329,7 +329,7 @@ def sort_expression(sort_by, sort_order=None):
329329
# the sortOrder parameter, i.e., they are ordered last if ascending and
330330
# first if descending", which is the default of PostgreSQL and not of the
331331
# engines taking NULL for the smallest value.
332-
if sort_order == SearchRequest.SortOrder.descending:
332+
if search_request.sort_order == SearchRequest.SortOrder.descending:
333333
return nulls_first(column.desc())
334334
return nulls_last(column.asc())
335335

@@ -361,9 +361,7 @@ def query_users(session, search_request):
361361

362362
order_by = []
363363
if search_request.sort_by:
364-
order_by.append(
365-
sort_expression(search_request.sort_by, search_request.sort_order)
366-
)
364+
order_by.append(sort_expression(search_request))
367365
# A page is a slice of an ordered result. Rows sharing a sort key, or a
368366
# query with no sortBy at all, leave OFFSET free to return one row twice
369367
# and another never, so the primary key always closes the order.

‎doc/integrations/sqlalchemy.rst‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,13 @@ The test on the case reads the SCIM type and not the Python one: ``emails.value`
121121
Sorting
122122
-------
123123

124-
:attr:`SearchRequest.sort_by <scim2_models.SearchRequest.sort_by>` is a
125-
:class:`~scim2_models.Path`, already resolved against the model on a parameterized request. On a
126-
union, it resolves against the first resource type declaring the attribute. ``sort_expression``
127-
looks the path up in the same table of columns as the filter does. A sub-attribute is filed under
124+
:meth:`SearchRequest.sort_binding <scim2_models.SearchRequest.sort_binding>` binds
125+
:attr:`~scim2_models.SearchRequest.sort_by` to an attribute of the model passed. Here the model is
126+
:class:`~scim2_models.User`, the type the table stores, so a request bound to a union or to no
127+
type at all sorts the same way. The method returns :data:`None` when users cannot be sorted on the
128+
attribute: unknown, complex, binary or write-only. A multi-valued complex attribute is bound to
129+
its ``value`` sub-attribute, so ``emails`` sorts as ``emails.value``. ``sort_expression`` looks
130+
the binding up in the same table of columns as the filter does. A sub-attribute is filed under
128131
the attribute holding it, so ``meta.lastModified`` is found at ``("meta", "last_modified")``. The
129132
remaining step turns the column into an ``ORDER BY`` term, and
130133
:rfc:`RFC7644 §3.4.2.3 <7644#section-3.4.2.3>` sets order rules that a bare ``ORDER BY column``

‎tests/test_doc_examples.py‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from doc.integrations._examples.sqlalchemy_example import to_scim_user # noqa: E402
2727
from scim2_models import BulkRequest # noqa: E402
2828
from scim2_models import Context # noqa: E402
29+
from scim2_models import Group # noqa: E402
2930
from scim2_models import InvalidPathException # noqa: E402
3031
from scim2_models import ScimFilter # noqa: E402
3132
from scim2_models import SearchRequest # noqa: E402
@@ -829,10 +830,23 @@ def test_sqlalchemy_rejects_sorting_on_an_unreachable_attribute(
829830
query_users(sqlalchemy_session, SearchRequest[User](sort_by=attribute))
830831

831832

832-
def test_sqlalchemy_rejects_a_request_that_named_no_resource_type(sqlalchemy_session):
833-
"""The query orders by a resolved attribute, which an unparameterised request has none of."""
833+
@pytest.mark.parametrize("request_type", [SearchRequest, SearchRequest[User | Group]])
834+
def test_sqlalchemy_sorts_a_request_bound_to_any_type_as_users(
835+
sqlalchemy_session, request_type
836+
):
837+
"""The table stores users, so the sortBy is resolved against User whatever the request names."""
838+
expected = query_users(sqlalchemy_session, SearchRequest[User](sort_by="userName"))
839+
total, page = query_users(sqlalchemy_session, request_type(sort_by="userName"))
840+
assert (total, page) == expected
841+
842+
843+
@pytest.mark.parametrize("sort_by", ["password", "name", "nonexistent"])
844+
def test_sqlalchemy_rejects_an_attribute_users_cannot_be_sorted_on(
845+
sqlalchemy_session, sort_by
846+
):
847+
"""An unparameterised request is not validated, so the query refuses the order itself."""
834848
with pytest.raises(InvalidPathException):
835-
query_users(sqlalchemy_session, SearchRequest(sort_by="userName"))
849+
query_users(sqlalchemy_session, SearchRequest(sort_by=sort_by))
836850

837851

838852
USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User"

0 commit comments

Comments
 (0)