Skip to content

Commit 23071dc

Browse files
committed
fix: order strings by code point on PostgreSQL
Comparisons and sorts on strings now use the C collation, so they follow the order of scim2-models. Before, they followed the collation of the column.
1 parent 54a40c4 commit 23071dc

3 files changed

Lines changed: 34 additions & 6 deletions

File tree

‎doc/explanation.rst‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,10 @@ database allows:
6969
This approaches the default key without reaching it. ``lower()`` follows the locale of the
7070
database, and only lowers ASCII letters under the ``C`` locale. Even under a UTF-8 locale, a
7171
few letters differ, such as ``İ`` and the final sigma. ``normalize()`` needs PostgreSQL 13 and
72-
a database encoded in UTF-8. An index that serves these filters is an index on the same
73-
expression. With a key other than the default one, the storage warns that it only approaches
74-
the key.
72+
a database encoded in UTF-8. The comparison uses the ``C`` collation, which orders strings by
73+
code point, as scim2-models does, whatever the collation of the column. An index that serves
74+
these filters is an index on the same expression, with the same collation. With a key other
75+
than the default one, the storage warns that it only approaches the key.
7576
- Other databases lower both sides with their own ``lower()``. MySQL and MariaDB are not
7677
supported: their default collations ignore the case and the accents, even for a ``caseExact``
7778
attribute.

‎src/scim2_sqlalchemy/comparison.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,15 @@ class _Normalizer(_Comparator):
7575
"""Compare strings in PostgreSQL, normalized to NFC on both sides.
7676
7777
The lower() of PostgreSQL follows the locale of the database, so the
78-
comparison approaches the default comparison key without reaching it.
78+
comparison approaches the default comparison key without reaching it. The
79+
C collation orders the strings by code point, as scim2-models does.
7980
"""
8081

8182
def _form(self, column: _Column, expression: Any) -> ColumnElement[Any]:
8283
normalized = func.normalize(expression, literal_column("NFC"))
8384
if column.casefolded:
84-
return func.normalize(func.lower(normalized), literal_column("NFC"))
85-
return normalized
85+
normalized = func.normalize(func.lower(normalized), literal_column("NFC"))
86+
return normalized.collate("C")
8687

8788
def column(self, column: _Column) -> ColumnElement[Any]:
8889
return self._form(column, column.compared)

‎tests/test_comparison.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from scim2_models import default_comparison_key
1313
from scim2_server.utils import load_default_service_provider_config
1414
from sqlalchemy import select
15+
from sqlalchemy import text
1516
from sqlalchemy.dialects import mysql
1617

1718
from scim2_sqlalchemy.comparison import _comparator
@@ -228,3 +229,28 @@ def test_postgresql_approaches_the_default_key(session, database_url, value):
228229
assert session.scalar(select(expression)) == default_comparison_key(
229230
column.binding, value
230231
)
232+
233+
234+
@pytest.mark.parametrize(
235+
("parameters", "expected"),
236+
[
237+
({"sort_by": "userName"}, ["e", "f", "é"]),
238+
({"filter": 'userName lt "f"'}, ["e"]),
239+
],
240+
)
241+
def test_postgresql_orders_strings_by_code_point(
242+
sync_storage_factory, session, database_url, user_type, parameters, expected
243+
):
244+
"""The order of scim2-models holds whatever the collation of the column."""
245+
if database_url.startswith("sqlite"):
246+
pytest.skip("SQLite orders strings by code point")
247+
session.execute(
248+
text(
249+
'ALTER TABLE users ALTER COLUMN user_name TYPE varchar COLLATE "und-x-icu"'
250+
)
251+
)
252+
storage = sync_storage_factory()
253+
for name in ("é", "f", "e"):
254+
storage.create(user_type, UserModel(user_name=name))
255+
256+
assert user_names(storage, user_type, **parameters) == expected

0 commit comments

Comments
 (0)