sqlakit 0.3.0__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.0 → sqlakit-0.4.0}/PKG-INFO +39 -39
- {sqlakit-0.3.0 → sqlakit-0.4.0}/README.md +38 -38
- {sqlakit-0.3.0 → sqlakit-0.4.0}/pyproject.toml +3 -3
- {sqlakit-0.3.0 → sqlakit-0.4.0}/pyproject.toml.orig +3 -5
- {sqlakit-0.3.0 → sqlakit-0.4.0}/sqlakit/__init__.py +2 -1
- {sqlakit-0.3.0 → sqlakit-0.4.0}/sqlakit/_query.py +67 -12
- {sqlakit-0.3.0 → sqlakit-0.4.0}/sqlakit/asyncio/orm.py +7 -2
- {sqlakit-0.3.0 → sqlakit-0.4.0}/sqlakit/orm.py +7 -2
- {sqlakit-0.3.0 → sqlakit-0.4.0}/LICENSE +0 -0
- {sqlakit-0.3.0 → sqlakit-0.4.0}/sqlakit/_base.py +0 -0
- {sqlakit-0.3.0 → sqlakit-0.4.0}/sqlakit/_db.py +0 -0
- {sqlakit-0.3.0 → sqlakit-0.4.0}/sqlakit/_discovery.py +0 -0
- {sqlakit-0.3.0 → sqlakit-0.4.0}/sqlakit/_model.py +0 -0
- {sqlakit-0.3.0 → sqlakit-0.4.0}/sqlakit/_recording.py +0 -0
- {sqlakit-0.3.0 → sqlakit-0.4.0}/sqlakit/_registry.py +0 -0
- {sqlakit-0.3.0 → sqlakit-0.4.0}/sqlakit/_routing.py +0 -0
- {sqlakit-0.3.0 → sqlakit-0.4.0}/sqlakit/_sql.py +0 -0
- {sqlakit-0.3.0 → sqlakit-0.4.0}/sqlakit/asyncio/__init__.py +0 -0
- {sqlakit-0.3.0 → sqlakit-0.4.0}/sqlakit/asyncio/_db.py +0 -0
- {sqlakit-0.3.0 → sqlakit-0.4.0}/sqlakit/asyncio/_registry.py +0 -0
- {sqlakit-0.3.0 → sqlakit-0.4.0}/sqlakit/asyncio/sql.py +0 -0
- {sqlakit-0.3.0 → sqlakit-0.4.0}/sqlakit/exceptions.py +0 -0
- {sqlakit-0.3.0 → sqlakit-0.4.0}/sqlakit/py.typed +0 -0
- {sqlakit-0.3.0 → sqlakit-0.4.0}/sqlakit/sql.py +0 -0
- {sqlakit-0.3.0 → sqlakit-0.4.0}/sqlakit/testing.py +0 -0
- {sqlakit-0.3.0 → 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
|
|
@@ -185,44 +185,6 @@ feed.next_cursor
|
|
|
185
185
|
feed.previous_cursor
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
-
## Active Record
|
|
189
|
-
|
|
190
|
-
An instance saves and deletes itself, and the query is available on the class:
|
|
191
|
-
|
|
192
|
-
```python
|
|
193
|
-
from sqlalchemy.orm import Mapped, mapped_column
|
|
194
|
-
|
|
195
|
-
from sqlakit import Database
|
|
196
|
-
from sqlakit.orm import Model
|
|
197
|
-
|
|
198
|
-
db = Database("postgresql+psycopg://localhost/app")
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
class Note(Model):
|
|
202
|
-
__tablename__ = "notes"
|
|
203
|
-
|
|
204
|
-
id: Mapped[int] = mapped_column(primary_key=True)
|
|
205
|
-
text: Mapped[str]
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
Note.set_db(db)
|
|
209
|
-
|
|
210
|
-
with db.transaction():
|
|
211
|
-
note = Note(text="ada")
|
|
212
|
-
note.save()
|
|
213
|
-
|
|
214
|
-
Note.query.where(Note.text == "ada").all()
|
|
215
|
-
note.delete()
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
`set_db()` binds a model to a database. Call it on a base class and every model
|
|
219
|
-
under it inherits the binding. With the global `db` from the section below you
|
|
220
|
-
don't need it at all: the model uses the global registry automatically.
|
|
221
|
-
|
|
222
|
-
This layer is optional. Everything else works on plain `SQLAlchemy` models, so
|
|
223
|
-
if saving belongs in your repositories or services, skip `sqlakit.orm`
|
|
224
|
-
entirely.
|
|
225
|
-
|
|
226
188
|
## Testing
|
|
227
189
|
|
|
228
190
|
A test runs inside a transaction that rolls back at the end, so nothing the
|
|
@@ -328,6 +290,42 @@ with db.using("replica").connect():
|
|
|
328
290
|
list_users() # the models read the replica
|
|
329
291
|
```
|
|
330
292
|
|
|
293
|
+
## Active Record
|
|
294
|
+
|
|
295
|
+
An instance saves and deletes itself, and the query is available on the class.
|
|
296
|
+
A model on the registry needs no wiring of its own:
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
300
|
+
|
|
301
|
+
from sqlakit import db
|
|
302
|
+
from sqlakit.orm import Model
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class Note(Model):
|
|
306
|
+
__tablename__ = "notes"
|
|
307
|
+
|
|
308
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
309
|
+
text: Mapped[str]
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
with db.transaction():
|
|
313
|
+
note = Note(text="ada").save()
|
|
314
|
+
|
|
315
|
+
Note.query.where(Note.text == "ada").all()
|
|
316
|
+
note.delete()
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
A model that belongs on another database in the registry names its alias with
|
|
320
|
+
`__db__ = "warehouse"`. With a `Database` of your own, `set_db()` binds the
|
|
321
|
+
model to it. Either goes on a base class, and every model under it inherits
|
|
322
|
+
the binding.
|
|
323
|
+
|
|
324
|
+
This layer is optional. Everything else works on plain `SQLAlchemy` models, so
|
|
325
|
+
if saving belongs in your repositories or services, skip `sqlakit.orm`
|
|
326
|
+
entirely. `SQLModel` classes are `SQLAlchemy` models, and work either way: the
|
|
327
|
+
[examples](examples/) show both.
|
|
328
|
+
|
|
331
329
|
## The async API
|
|
332
330
|
|
|
333
331
|
The async API is identical: the same classes, the same methods. Only the import
|
|
@@ -408,3 +406,5 @@ test from an empty file. The rest is under [`docs/`](docs/):
|
|
|
408
406
|
[debugging](docs/debugging.md), [multiple databases](docs/routing.md) and
|
|
409
407
|
[the reference](docs/reference.md). Complete example apps live in
|
|
410
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).
|
|
@@ -153,44 +153,6 @@ feed.next_cursor
|
|
|
153
153
|
feed.previous_cursor
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
-
## Active Record
|
|
157
|
-
|
|
158
|
-
An instance saves and deletes itself, and the query is available on the class:
|
|
159
|
-
|
|
160
|
-
```python
|
|
161
|
-
from sqlalchemy.orm import Mapped, mapped_column
|
|
162
|
-
|
|
163
|
-
from sqlakit import Database
|
|
164
|
-
from sqlakit.orm import Model
|
|
165
|
-
|
|
166
|
-
db = Database("postgresql+psycopg://localhost/app")
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
class Note(Model):
|
|
170
|
-
__tablename__ = "notes"
|
|
171
|
-
|
|
172
|
-
id: Mapped[int] = mapped_column(primary_key=True)
|
|
173
|
-
text: Mapped[str]
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
Note.set_db(db)
|
|
177
|
-
|
|
178
|
-
with db.transaction():
|
|
179
|
-
note = Note(text="ada")
|
|
180
|
-
note.save()
|
|
181
|
-
|
|
182
|
-
Note.query.where(Note.text == "ada").all()
|
|
183
|
-
note.delete()
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
`set_db()` binds a model to a database. Call it on a base class and every model
|
|
187
|
-
under it inherits the binding. With the global `db` from the section below you
|
|
188
|
-
don't need it at all: the model uses the global registry automatically.
|
|
189
|
-
|
|
190
|
-
This layer is optional. Everything else works on plain `SQLAlchemy` models, so
|
|
191
|
-
if saving belongs in your repositories or services, skip `sqlakit.orm`
|
|
192
|
-
entirely.
|
|
193
|
-
|
|
194
156
|
## Testing
|
|
195
157
|
|
|
196
158
|
A test runs inside a transaction that rolls back at the end, so nothing the
|
|
@@ -296,6 +258,42 @@ with db.using("replica").connect():
|
|
|
296
258
|
list_users() # the models read the replica
|
|
297
259
|
```
|
|
298
260
|
|
|
261
|
+
## Active Record
|
|
262
|
+
|
|
263
|
+
An instance saves and deletes itself, and the query is available on the class.
|
|
264
|
+
A model on the registry needs no wiring of its own:
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
268
|
+
|
|
269
|
+
from sqlakit import db
|
|
270
|
+
from sqlakit.orm import Model
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class Note(Model):
|
|
274
|
+
__tablename__ = "notes"
|
|
275
|
+
|
|
276
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
277
|
+
text: Mapped[str]
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
with db.transaction():
|
|
281
|
+
note = Note(text="ada").save()
|
|
282
|
+
|
|
283
|
+
Note.query.where(Note.text == "ada").all()
|
|
284
|
+
note.delete()
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
A model that belongs on another database in the registry names its alias with
|
|
288
|
+
`__db__ = "warehouse"`. With a `Database` of your own, `set_db()` binds the
|
|
289
|
+
model to it. Either goes on a base class, and every model under it inherits
|
|
290
|
+
the binding.
|
|
291
|
+
|
|
292
|
+
This layer is optional. Everything else works on plain `SQLAlchemy` models, so
|
|
293
|
+
if saving belongs in your repositories or services, skip `sqlakit.orm`
|
|
294
|
+
entirely. `SQLModel` classes are `SQLAlchemy` models, and work either way: the
|
|
295
|
+
[examples](examples/) show both.
|
|
296
|
+
|
|
299
297
|
## The async API
|
|
300
298
|
|
|
301
299
|
The async API is identical: the same classes, the same methods. Only the import
|
|
@@ -376,3 +374,5 @@ test from an empty file. The rest is under [`docs/`](docs/):
|
|
|
376
374
|
[debugging](docs/debugging.md), [multiple databases](docs/routing.md) and
|
|
377
375
|
[the reference](docs/reference.md). Complete example apps live in
|
|
378
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,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sqlakit"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "A toolkit for SQLAlchemy applications."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -163,8 +163,8 @@ skip = "./.venv,./site,./uv.lock,./.git"
|
|
|
163
163
|
[dependency-groups]
|
|
164
164
|
dev = [
|
|
165
165
|
"pytest>=9.1.1",
|
|
166
|
-
"ty>=0.0.
|
|
167
|
-
"ruff>=0.16.
|
|
166
|
+
"ty>=0.0.75",
|
|
167
|
+
"ruff>=0.16.5",
|
|
168
168
|
"poethepoet>=0.48.0",
|
|
169
169
|
"anyio>=4.14.2",
|
|
170
170
|
"trio>=0.34.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sqlakit"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "A toolkit for SQLAlchemy applications."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -30,8 +30,6 @@ Repository = "https://github.com/antonrh/sqlakit"
|
|
|
30
30
|
Documentation = "https://github.com/antonrh/sqlakit"
|
|
31
31
|
|
|
32
32
|
[project.optional-dependencies]
|
|
33
|
-
# `greenlet`, which the async API runs on. `SQLAlchemy` installs it by itself
|
|
34
|
-
# where a wheel exists, and this asks for it on the platforms where none does.
|
|
35
33
|
asyncio = [
|
|
36
34
|
"sqlalchemy[asyncio]>=2.0.22",
|
|
37
35
|
]
|
|
@@ -56,8 +54,8 @@ default-groups = ["dev", "docs"]
|
|
|
56
54
|
[dependency-groups]
|
|
57
55
|
dev = [
|
|
58
56
|
"pytest>=9.1.1",
|
|
59
|
-
"ty>=0.0.
|
|
60
|
-
"ruff>=0.16.
|
|
57
|
+
"ty>=0.0.75",
|
|
58
|
+
"ruff>=0.16.5",
|
|
61
59
|
"poethepoet>=0.48.0",
|
|
62
60
|
"anyio>=4.14.2",
|
|
63
61
|
"trio>=0.34.0",
|
|
@@ -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
|