sqlakit 0.3.1__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sqlakit-0.3.1 → sqlakit-0.4.0}/PKG-INFO +3 -1
- {sqlakit-0.3.1 → sqlakit-0.4.0}/README.md +2 -0
- {sqlakit-0.3.1 → sqlakit-0.4.0}/pyproject.toml +1 -1
- {sqlakit-0.3.1 → sqlakit-0.4.0}/pyproject.toml.orig +1 -1
- {sqlakit-0.3.1 → sqlakit-0.4.0}/sqlakit/__init__.py +2 -1
- {sqlakit-0.3.1 → sqlakit-0.4.0}/sqlakit/_query.py +67 -12
- {sqlakit-0.3.1 → sqlakit-0.4.0}/sqlakit/asyncio/orm.py +7 -2
- {sqlakit-0.3.1 → sqlakit-0.4.0}/sqlakit/orm.py +7 -2
- {sqlakit-0.3.1 → sqlakit-0.4.0}/LICENSE +0 -0
- {sqlakit-0.3.1 → sqlakit-0.4.0}/sqlakit/_base.py +0 -0
- {sqlakit-0.3.1 → sqlakit-0.4.0}/sqlakit/_db.py +0 -0
- {sqlakit-0.3.1 → sqlakit-0.4.0}/sqlakit/_discovery.py +0 -0
- {sqlakit-0.3.1 → sqlakit-0.4.0}/sqlakit/_model.py +0 -0
- {sqlakit-0.3.1 → sqlakit-0.4.0}/sqlakit/_recording.py +0 -0
- {sqlakit-0.3.1 → sqlakit-0.4.0}/sqlakit/_registry.py +0 -0
- {sqlakit-0.3.1 → sqlakit-0.4.0}/sqlakit/_routing.py +0 -0
- {sqlakit-0.3.1 → sqlakit-0.4.0}/sqlakit/_sql.py +0 -0
- {sqlakit-0.3.1 → sqlakit-0.4.0}/sqlakit/asyncio/__init__.py +0 -0
- {sqlakit-0.3.1 → sqlakit-0.4.0}/sqlakit/asyncio/_db.py +0 -0
- {sqlakit-0.3.1 → sqlakit-0.4.0}/sqlakit/asyncio/_registry.py +0 -0
- {sqlakit-0.3.1 → sqlakit-0.4.0}/sqlakit/asyncio/sql.py +0 -0
- {sqlakit-0.3.1 → sqlakit-0.4.0}/sqlakit/exceptions.py +0 -0
- {sqlakit-0.3.1 → sqlakit-0.4.0}/sqlakit/py.typed +0 -0
- {sqlakit-0.3.1 → sqlakit-0.4.0}/sqlakit/sql.py +0 -0
- {sqlakit-0.3.1 → sqlakit-0.4.0}/sqlakit/testing.py +0 -0
- {sqlakit-0.3.1 → sqlakit-0.4.0}/sqlakit/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlakit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A toolkit for SQLAlchemy applications.
|
|
5
5
|
Keywords: sqlalchemy,database,orm,sql,asyncio
|
|
6
6
|
Author: Anton Ruhlov
|
|
@@ -406,3 +406,5 @@ test from an empty file. The rest is under [`docs/`](docs/):
|
|
|
406
406
|
[debugging](docs/debugging.md), [multiple databases](docs/routing.md) and
|
|
407
407
|
[the reference](docs/reference.md). Complete example apps live in
|
|
408
408
|
[`examples/`](examples/), and each one is run by the test suite.
|
|
409
|
+
|
|
410
|
+
What changed in each version is in the [changelog](CHANGELOG.md).
|
|
@@ -374,3 +374,5 @@ test from an empty file. The rest is under [`docs/`](docs/):
|
|
|
374
374
|
[debugging](docs/debugging.md), [multiple databases](docs/routing.md) and
|
|
375
375
|
[the reference](docs/reference.md). Complete example apps live in
|
|
376
376
|
[`examples/`](examples/), and each one is run by the test suite.
|
|
377
|
+
|
|
378
|
+
What changed in each version is in the [changelog](CHANGELOG.md).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from ._base import DEFAULT_ENGINE_ARGS, DEFAULT_SESSION_ARGS
|
|
2
2
|
from ._db import Database, RetryingTransaction, Transaction
|
|
3
3
|
from ._discovery import import_models, import_string
|
|
4
|
-
from ._query import CursorPage, OrderBy, Page
|
|
4
|
+
from ._query import CASE_INSENSITIVE_COLLATIONS, CursorPage, OrderBy, Page
|
|
5
5
|
from ._recording import Recording, Statement
|
|
6
6
|
from ._registry import Databases, db
|
|
7
7
|
from ._routing import Router
|
|
@@ -43,6 +43,7 @@ from .exceptions import (
|
|
|
43
43
|
from .types import DatabaseConfig, EngineArgs, QueryStats, SessionArgs, UrlParts
|
|
44
44
|
|
|
45
45
|
__all__ = [
|
|
46
|
+
"CASE_INSENSITIVE_COLLATIONS",
|
|
46
47
|
"DEFAULT_ALIAS",
|
|
47
48
|
"DEFAULT_ENGINE_ARGS",
|
|
48
49
|
"DEFAULT_SESSION_ARGS",
|
|
@@ -19,6 +19,7 @@ from typing import (
|
|
|
19
19
|
|
|
20
20
|
import sqlalchemy as sa
|
|
21
21
|
from sqlalchemy import exc as sa_exc
|
|
22
|
+
from sqlalchemy.ext.compiler import compiles
|
|
22
23
|
from sqlalchemy.orm import (
|
|
23
24
|
InstrumentedAttribute,
|
|
24
25
|
contains_eager,
|
|
@@ -53,7 +54,9 @@ if TYPE_CHECKING:
|
|
|
53
54
|
from sqlalchemy.sql.selectable import ForUpdateParameter
|
|
54
55
|
|
|
55
56
|
__all__ = [
|
|
57
|
+
"CASE_INSENSITIVE_COLLATIONS",
|
|
56
58
|
"BaseQuery",
|
|
59
|
+
"CaseInsensitive",
|
|
57
60
|
"CursorPage",
|
|
58
61
|
"OrderBy",
|
|
59
62
|
"Page",
|
|
@@ -63,6 +66,21 @@ __all__ = [
|
|
|
63
66
|
"ordered",
|
|
64
67
|
]
|
|
65
68
|
|
|
69
|
+
CASE_INSENSITIVE_COLLATIONS: dict[str, str] = {"sqlite": "NOCASE"}
|
|
70
|
+
"""The collation `ignore_case` orders by, per dialect.
|
|
71
|
+
|
|
72
|
+
A dialect with no entry orders by `lower(...)`, which every database has.
|
|
73
|
+
Name the collation you created, once, before any query runs:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
sqlakit.CASE_INSENSITIVE_COLLATIONS["postgresql"] = "und-ci-ai"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A collation decides the whole order, the alphabet and the accents along with
|
|
80
|
+
the case. This one is asked for only by `ignore_case`. To sort by another,
|
|
81
|
+
name it on the column: `User.name.collate("de-DE")`.
|
|
82
|
+
"""
|
|
83
|
+
|
|
66
84
|
HIDDEN = "hidden"
|
|
67
85
|
"""The rows a soft delete marked are left out, as a read does by default."""
|
|
68
86
|
|
|
@@ -78,6 +96,33 @@ RowT = TypeVar("RowT")
|
|
|
78
96
|
RowT_co = TypeVar("RowT_co", covariant=True)
|
|
79
97
|
|
|
80
98
|
|
|
99
|
+
class CaseInsensitive(sa.ColumnElement[Any]):
|
|
100
|
+
"""A column compared without regard to case, however the dialect does it.
|
|
101
|
+
|
|
102
|
+
The dialect is the one the query runs on, not the one it was built against,
|
|
103
|
+
so a model ordered this way works on `SQLite` under test and on the server
|
|
104
|
+
it ships to.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
inherit_cache = True
|
|
108
|
+
|
|
109
|
+
def __init__(self, element: sa.ColumnElement[Any]) -> None:
|
|
110
|
+
self.element = element
|
|
111
|
+
self.type = element.type
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@compiles(CaseInsensitive)
|
|
115
|
+
def _compile_case_insensitive(
|
|
116
|
+
element: CaseInsensitive,
|
|
117
|
+
compiler: Any, # noqa: ANN401
|
|
118
|
+
**kw: Any, # noqa: ANN401
|
|
119
|
+
) -> str:
|
|
120
|
+
collation = CASE_INSENSITIVE_COLLATIONS.get(compiler.dialect.name)
|
|
121
|
+
if collation is None:
|
|
122
|
+
return compiler.process(sa.func.lower(element.element), **kw)
|
|
123
|
+
return compiler.process(sa.collate(element.element, collation), **kw)
|
|
124
|
+
|
|
125
|
+
|
|
81
126
|
@dataclass(frozen=True, slots=True)
|
|
82
127
|
class Page(Generic[ModelT]):
|
|
83
128
|
"""One page of rows, and how many there are in total."""
|
|
@@ -420,7 +465,7 @@ class BaseQuery(Generic[ModelT]):
|
|
|
420
465
|
def order_by(
|
|
421
466
|
self,
|
|
422
467
|
*criteria: Any, # noqa: ANN401
|
|
423
|
-
|
|
468
|
+
ignore_case: bool | Sequence[str] = False,
|
|
424
469
|
) -> Self:
|
|
425
470
|
"""Order the rows, by columns or by the sort strings a request carries.
|
|
426
471
|
|
|
@@ -437,9 +482,18 @@ class BaseQuery(Generic[ModelT]):
|
|
|
437
482
|
A `None` is skipped and a list is taken apart, so a request that names no
|
|
438
483
|
sort, or several, passes straight through.
|
|
439
484
|
|
|
440
|
-
``
|
|
441
|
-
|
|
442
|
-
|
|
485
|
+
``ignore_case`` compares text without regard to case: `True` for every
|
|
486
|
+
field of this call, or the names of the ones it applies to, for a sort
|
|
487
|
+
that arrived as a list:
|
|
488
|
+
|
|
489
|
+
```python
|
|
490
|
+
User.query.order_by("name", ignore_case=True)
|
|
491
|
+
User.query.order_by(request.sort, ignore_case=["name"])
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Which SQL that becomes is the dialect's to decide, and
|
|
495
|
+
`CASE_INSENSITIVE_COLLATIONS` names the collation. A cursor cannot page
|
|
496
|
+
it: use it with `page`.
|
|
443
497
|
|
|
444
498
|
A model sorts by its own mapped columns. `orderable` says how to offer
|
|
445
499
|
others, including fields that are not columns at all.
|
|
@@ -451,7 +505,7 @@ class BaseQuery(Generic[ModelT]):
|
|
|
451
505
|
"""
|
|
452
506
|
self._reject_statement("order_by")
|
|
453
507
|
return self.with_select(
|
|
454
|
-
ordered(self._select, self._orderable(), criteria,
|
|
508
|
+
ordered(self._select, self._orderable(), criteria, ignore_case=ignore_case)
|
|
455
509
|
)
|
|
456
510
|
|
|
457
511
|
def _directed(self, column: Any, *, descending: bool) -> Any: # noqa: ANN401
|
|
@@ -836,7 +890,8 @@ def ordered(
|
|
|
836
890
|
select: sa.Select[Any],
|
|
837
891
|
fields: Mapping[str, Any],
|
|
838
892
|
criteria: Iterable[Any],
|
|
839
|
-
|
|
893
|
+
*,
|
|
894
|
+
ignore_case: bool | Sequence[str] = False,
|
|
840
895
|
) -> sa.Select[Any]:
|
|
841
896
|
"""Return the statement ordered by these criteria, joining what they need.
|
|
842
897
|
|
|
@@ -847,10 +902,9 @@ def ordered(
|
|
|
847
902
|
named = list(_flatten(criteria))
|
|
848
903
|
if not named:
|
|
849
904
|
return select
|
|
850
|
-
ci = set(ci_fields)
|
|
851
905
|
clauses = []
|
|
852
906
|
for criterion in named:
|
|
853
|
-
clause, join = _ordering_for(criterion, fields,
|
|
907
|
+
clause, join = _ordering_for(criterion, fields, ignore_case=ignore_case)
|
|
854
908
|
clauses.append(clause)
|
|
855
909
|
if join is not None:
|
|
856
910
|
target, onclause = join
|
|
@@ -862,7 +916,8 @@ def ordered(
|
|
|
862
916
|
def _ordering_for(
|
|
863
917
|
criterion: Any, # noqa: ANN401
|
|
864
918
|
fields: Mapping[str, Any],
|
|
865
|
-
|
|
919
|
+
*,
|
|
920
|
+
ignore_case: bool | Sequence[str],
|
|
866
921
|
) -> tuple[Any, Any]:
|
|
867
922
|
"""Return the clause a criterion stands for, and the table it needs."""
|
|
868
923
|
if isinstance(criterion, OrderBy):
|
|
@@ -877,7 +932,7 @@ def _ordering_for(
|
|
|
877
932
|
column, join = field.expression, (field.join, field.on)
|
|
878
933
|
else:
|
|
879
934
|
column, join = field, None
|
|
880
|
-
if name in
|
|
935
|
+
if ignore_case is True or (ignore_case and name in ignore_case):
|
|
881
936
|
column = _case_insensitive(column)
|
|
882
937
|
return _sort_clause(column, descending=descending, nulls=nulls), join
|
|
883
938
|
|
|
@@ -990,11 +1045,11 @@ def _selectable_of(target: Any) -> Any: # noqa: ANN401
|
|
|
990
1045
|
|
|
991
1046
|
|
|
992
1047
|
def _case_insensitive(column: Any) -> Any: # noqa: ANN401
|
|
993
|
-
"""Return the column
|
|
1048
|
+
"""Return the column compared without regard to case, if it holds text."""
|
|
994
1049
|
inner, nulls = _split_nulls(column)
|
|
995
1050
|
if not isinstance(getattr(inner, "type", None), sa.String):
|
|
996
1051
|
return column
|
|
997
|
-
folded =
|
|
1052
|
+
folded = CaseInsensitive(inner)
|
|
998
1053
|
if nulls == "nulls_last":
|
|
999
1054
|
return sa.nulls_last(folded)
|
|
1000
1055
|
if nulls == "nulls_first":
|
|
@@ -432,11 +432,16 @@ class ColumnQuery(Generic[RowT]):
|
|
|
432
432
|
def order_by(
|
|
433
433
|
self,
|
|
434
434
|
*criteria: Any, # noqa: ANN401
|
|
435
|
-
|
|
435
|
+
ignore_case: bool | Sequence[str] = False,
|
|
436
436
|
) -> Self:
|
|
437
437
|
"""Order the rows, by columns or by the names the model offers."""
|
|
438
438
|
return self.with_select(
|
|
439
|
-
ordered(
|
|
439
|
+
ordered(
|
|
440
|
+
self._select,
|
|
441
|
+
orderable(self.model),
|
|
442
|
+
criteria,
|
|
443
|
+
ignore_case=ignore_case,
|
|
444
|
+
)
|
|
440
445
|
)
|
|
441
446
|
|
|
442
447
|
def distinct(self) -> Self:
|
|
@@ -420,11 +420,16 @@ class ColumnQuery(Generic[RowT]):
|
|
|
420
420
|
def order_by(
|
|
421
421
|
self,
|
|
422
422
|
*criteria: Any, # noqa: ANN401
|
|
423
|
-
|
|
423
|
+
ignore_case: bool | Sequence[str] = False,
|
|
424
424
|
) -> Self:
|
|
425
425
|
"""Order the rows, by columns or by the names the model offers."""
|
|
426
426
|
return self.with_select(
|
|
427
|
-
ordered(
|
|
427
|
+
ordered(
|
|
428
|
+
self._select,
|
|
429
|
+
orderable(self.model),
|
|
430
|
+
criteria,
|
|
431
|
+
ignore_case=ignore_case,
|
|
432
|
+
)
|
|
428
433
|
)
|
|
429
434
|
|
|
430
435
|
def distinct(self) -> Self:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|