ns-orm 0.0.2__tar.gz → 0.0.4__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.
- {ns_orm-0.0.2/src/ns_orm.egg-info → ns_orm-0.0.4}/PKG-INFO +1 -1
- {ns_orm-0.0.2 → ns_orm-0.0.4}/pyproject.toml +1 -1
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm/manager.py +9 -3
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm/model.py +5 -2
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm/query.py +293 -38
- {ns_orm-0.0.2 → ns_orm-0.0.4/src/ns_orm.egg-info}/PKG-INFO +1 -1
- {ns_orm-0.0.2 → ns_orm-0.0.4}/LICENSE +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/MANIFEST.in +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/README.md +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/doc/async.md +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/doc/database.md +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/doc/index.md +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/doc/migrations.md +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/doc/models.md +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/doc/queryset.md +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/doc/relations.md +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/doc/release.md +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/doc/schema.md +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/setup.cfg +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm/__init__.py +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm/cli.py +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm/database.py +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm/dialects.py +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm/exceptions.py +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm/expressions.py +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm/fields.py +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm/migrations/__init__.py +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm/migrations/autodetector.py +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm/migrations/executor.py +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm/migrations/loader.py +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm/migrations/migration.py +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm/migrations/operations.py +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm/migrations/state.py +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm/migrations/writer.py +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm/schema.py +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm/typing.py +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm/utils.py +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm.egg-info/SOURCES.txt +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm.egg-info/dependency_links.txt +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm.egg-info/entry_points.txt +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm.egg-info/requires.txt +0 -0
- {ns_orm-0.0.2 → ns_orm-0.0.4}/src/ns_orm.egg-info/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Any, Generic, TypeVar
|
|
4
|
+
from typing import Any, Generic, TypeVar, Union
|
|
5
5
|
|
|
6
6
|
from ns_orm.database import AsyncDatabase, Database, get_connection
|
|
7
7
|
from ns_orm.query import AsyncQuerySet, QuerySet
|
|
@@ -47,18 +47,24 @@ class Manager(Generic[TModel]):
|
|
|
47
47
|
async def aall(self) -> list[TModel]:
|
|
48
48
|
return await self.aqs().all()
|
|
49
49
|
|
|
50
|
-
def filter(self, **lookups: Any) ->
|
|
50
|
+
def filter(self, **lookups: Any) -> Union[QuerySet[TModel], AsyncQuerySet[TModel]]:
|
|
51
51
|
db = self._get_db()
|
|
52
52
|
if isinstance(db, AsyncDatabase):
|
|
53
53
|
return self.aqs().filter(**lookups)
|
|
54
54
|
return self.qs().filter(**lookups)
|
|
55
55
|
|
|
56
|
-
def exclude(self, **lookups: Any) ->
|
|
56
|
+
def exclude(self, **lookups: Any) -> Union[QuerySet[TModel], AsyncQuerySet[TModel]]:
|
|
57
57
|
db = self._get_db()
|
|
58
58
|
if isinstance(db, AsyncDatabase):
|
|
59
59
|
return self.aqs().exclude(**lookups)
|
|
60
60
|
return self.qs().exclude(**lookups)
|
|
61
61
|
|
|
62
|
+
def order_by(self, *fields: str) -> Union[QuerySet[TModel], AsyncQuerySet[TModel]]:
|
|
63
|
+
db = self._get_db()
|
|
64
|
+
if isinstance(db, AsyncDatabase):
|
|
65
|
+
return self.aqs().order_by(*fields)
|
|
66
|
+
return self.qs().order_by(*fields)
|
|
67
|
+
|
|
62
68
|
def get(self, **lookups: Any) -> TModel:
|
|
63
69
|
return self.qs().get(**lookups)
|
|
64
70
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
-
from typing import Any, ClassVar, Dict, Type, TypeVar
|
|
4
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Dict, Type, TypeVar
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel
|
|
7
7
|
|
|
@@ -18,6 +18,9 @@ from ns_orm.utils import (
|
|
|
18
18
|
|
|
19
19
|
TModel = TypeVar("TModel", bound="Model")
|
|
20
20
|
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from ns_orm.manager import Manager
|
|
23
|
+
|
|
21
24
|
|
|
22
25
|
def _default_table_name(cls_name: str) -> str:
|
|
23
26
|
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", cls_name)
|
|
@@ -31,7 +34,7 @@ class Model(BaseModel):
|
|
|
31
34
|
|
|
32
35
|
_meta: ClassVar[ModelMetaInfo]
|
|
33
36
|
_registry: ClassVar[Dict[str, Type["Model"]]] = {}
|
|
34
|
-
objects: ClassVar[Any]
|
|
37
|
+
objects: ClassVar["Manager[Any]"]
|
|
35
38
|
|
|
36
39
|
@classmethod
|
|
37
40
|
def table_name(cls) -> str:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, replace
|
|
4
|
-
from typing import Any, Generic, Optional, TypeVar
|
|
4
|
+
from typing import Any, Dict, Generic, Optional, TypeVar
|
|
5
5
|
|
|
6
6
|
from ns_orm.database import AsyncDatabase, Database
|
|
7
7
|
from ns_orm.dialects import Dialect
|
|
@@ -23,6 +23,155 @@ def _qualified_table(model: type[Any], dialect: Dialect) -> str:
|
|
|
23
23
|
return table
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
_LOOKUP_OPS = {
|
|
27
|
+
"exact",
|
|
28
|
+
"gt",
|
|
29
|
+
"gte",
|
|
30
|
+
"lt",
|
|
31
|
+
"lte",
|
|
32
|
+
"in",
|
|
33
|
+
"contains",
|
|
34
|
+
"icontains",
|
|
35
|
+
"startswith",
|
|
36
|
+
"istartswith",
|
|
37
|
+
"endswith",
|
|
38
|
+
"iendswith",
|
|
39
|
+
"isnull",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _parse_lookup(lookup: str) -> tuple[list[str], str, str]:
|
|
44
|
+
parts = lookup.split("__")
|
|
45
|
+
if not parts:
|
|
46
|
+
raise QueryError(f"Invalid lookup: {lookup}")
|
|
47
|
+
op = "exact"
|
|
48
|
+
if parts[-1] in _LOOKUP_OPS:
|
|
49
|
+
op = parts[-1]
|
|
50
|
+
path = parts[:-1]
|
|
51
|
+
else:
|
|
52
|
+
path = parts
|
|
53
|
+
if not path:
|
|
54
|
+
raise QueryError(f"Invalid lookup: {lookup}")
|
|
55
|
+
field = path[-1]
|
|
56
|
+
rel_path = path[:-1]
|
|
57
|
+
return rel_path, field, op
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class _JoinBuilder:
|
|
61
|
+
def __init__(self, *, model: type[Any], dialect: Dialect, base_alias: str) -> None:
|
|
62
|
+
self.model = model
|
|
63
|
+
self.dialect = dialect
|
|
64
|
+
self.base_alias = base_alias
|
|
65
|
+
self._joins: list[str] = []
|
|
66
|
+
self._aliases: Dict[tuple[str, str], str] = {}
|
|
67
|
+
self._targets: Dict[tuple[str, str], type[Any]] = {}
|
|
68
|
+
self._alias_counter: int = 0
|
|
69
|
+
self.needs_distinct: bool = False
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def joins(self) -> list[str]:
|
|
73
|
+
return list(self._joins)
|
|
74
|
+
|
|
75
|
+
def _new_alias(self) -> str:
|
|
76
|
+
self._alias_counter += 1
|
|
77
|
+
return f"t{self._alias_counter}"
|
|
78
|
+
|
|
79
|
+
def _resolve_to_model(self, from_model: type[Any], to_ref: Any) -> type[Any]:
|
|
80
|
+
if isinstance(to_ref, str):
|
|
81
|
+
return from_model._resolve_model(to_ref)
|
|
82
|
+
return to_ref
|
|
83
|
+
|
|
84
|
+
def _ensure_join(
|
|
85
|
+
self,
|
|
86
|
+
*,
|
|
87
|
+
from_model: type[Any],
|
|
88
|
+
from_alias: str,
|
|
89
|
+
rel_name: str,
|
|
90
|
+
) -> tuple[type[Any], str]:
|
|
91
|
+
key = (from_alias, rel_name)
|
|
92
|
+
if key in self._aliases:
|
|
93
|
+
return self._targets[key], self._aliases[key]
|
|
94
|
+
|
|
95
|
+
if rel_name not in from_model._meta.relations:
|
|
96
|
+
to_model, fk_col = self._resolve_reverse_relation(
|
|
97
|
+
from_model=from_model, rel_name=rel_name
|
|
98
|
+
)
|
|
99
|
+
alias = self._new_alias()
|
|
100
|
+
self._aliases[key] = alias
|
|
101
|
+
self._targets[key] = to_model
|
|
102
|
+
self.needs_distinct = True
|
|
103
|
+
|
|
104
|
+
left = f"{alias}.{self.dialect.quote_ident(fk_col)}"
|
|
105
|
+
right = f"{from_alias}.{self.dialect.quote_ident(from_model.pk_name())}"
|
|
106
|
+
join_sql = (
|
|
107
|
+
f"JOIN {_qualified_table(to_model, self.dialect)} {alias} "
|
|
108
|
+
f"ON {left} = {right}"
|
|
109
|
+
)
|
|
110
|
+
self._joins.append(join_sql)
|
|
111
|
+
return to_model, alias
|
|
112
|
+
|
|
113
|
+
fk_col, fk = from_model._meta.relations[rel_name]
|
|
114
|
+
to_model = self._resolve_to_model(from_model, fk.to)
|
|
115
|
+
alias = self._new_alias()
|
|
116
|
+
self._aliases[key] = alias
|
|
117
|
+
self._targets[key] = to_model
|
|
118
|
+
|
|
119
|
+
left = f"{from_alias}.{self.dialect.quote_ident(fk_col)}"
|
|
120
|
+
right = f"{alias}.{self.dialect.quote_ident(to_model.pk_name())}"
|
|
121
|
+
join_sql = (
|
|
122
|
+
f"JOIN {_qualified_table(to_model, self.dialect)} {alias} "
|
|
123
|
+
f"ON {left} = {right}"
|
|
124
|
+
)
|
|
125
|
+
self._joins.append(join_sql)
|
|
126
|
+
return to_model, alias
|
|
127
|
+
|
|
128
|
+
def _resolve_reverse_relation(
|
|
129
|
+
self, *, from_model: type[Any], rel_name: str
|
|
130
|
+
) -> tuple[type[Any], str]:
|
|
131
|
+
registry = getattr(from_model, "_registry", {})
|
|
132
|
+
seen: set[int] = set()
|
|
133
|
+
candidates: list[tuple[type[Any], str]] = []
|
|
134
|
+
|
|
135
|
+
for m in registry.values():
|
|
136
|
+
if not isinstance(m, type):
|
|
137
|
+
continue
|
|
138
|
+
if id(m) in seen:
|
|
139
|
+
continue
|
|
140
|
+
seen.add(id(m))
|
|
141
|
+
meta = getattr(m, "_meta", None)
|
|
142
|
+
if meta is None:
|
|
143
|
+
continue
|
|
144
|
+
for _child_rel_name, (fk_col, fk) in meta.relations.items():
|
|
145
|
+
to_model = self._resolve_to_model(m, fk.to)
|
|
146
|
+
if to_model is not from_model:
|
|
147
|
+
continue
|
|
148
|
+
names = {
|
|
149
|
+
m.__name__.lower(),
|
|
150
|
+
m.table_name(),
|
|
151
|
+
f"{m.__name__.lower()}_set",
|
|
152
|
+
f"{m.table_name()}_set",
|
|
153
|
+
f"{m.__name__.lower()}s",
|
|
154
|
+
f"{m.table_name()}s",
|
|
155
|
+
}
|
|
156
|
+
if rel_name in names:
|
|
157
|
+
candidates.append((m, fk_col))
|
|
158
|
+
|
|
159
|
+
if not candidates:
|
|
160
|
+
raise QueryError(f"Unknown relation: {rel_name}")
|
|
161
|
+
if len(candidates) > 1:
|
|
162
|
+
raise QueryError(f"Ambiguous reverse relation: {rel_name}")
|
|
163
|
+
return candidates[0]
|
|
164
|
+
|
|
165
|
+
def resolve(self, rel_path: list[str]) -> tuple[type[Any], str]:
|
|
166
|
+
model: type[Any] = self.model
|
|
167
|
+
alias = self.base_alias
|
|
168
|
+
for rel in rel_path:
|
|
169
|
+
model, alias = self._ensure_join(
|
|
170
|
+
from_model=model, from_alias=alias, rel_name=rel
|
|
171
|
+
)
|
|
172
|
+
return model, alias
|
|
173
|
+
|
|
174
|
+
|
|
26
175
|
def _where_from_q(
|
|
27
176
|
model: type[Any],
|
|
28
177
|
dialect: Dialect,
|
|
@@ -31,6 +180,8 @@ def _where_from_q(
|
|
|
31
180
|
table_alias: Optional[str] = None,
|
|
32
181
|
param_prefix: str = "p",
|
|
33
182
|
start_index: int = 1,
|
|
183
|
+
join_builder: Optional[_JoinBuilder] = None,
|
|
184
|
+
allow_joins: bool = True,
|
|
34
185
|
) -> tuple[str, dict[str, Any]]:
|
|
35
186
|
def _lookup_compiler(
|
|
36
187
|
*,
|
|
@@ -40,13 +191,19 @@ def _where_from_q(
|
|
|
40
191
|
param_prefix: str,
|
|
41
192
|
start_index: int,
|
|
42
193
|
) -> tuple[Condition, int]:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
194
|
+
rel_path, field, op = _parse_lookup(lookup)
|
|
195
|
+
|
|
196
|
+
target_model = model
|
|
197
|
+
target_alias = table_alias
|
|
198
|
+
if rel_path:
|
|
199
|
+
if not allow_joins or join_builder is None or not table_alias:
|
|
200
|
+
raise QueryError(f"Join lookup is not supported here: {lookup}")
|
|
201
|
+
target_model, target_alias = join_builder.resolve(rel_path)
|
|
202
|
+
|
|
203
|
+
if field not in target_model._meta.fields:
|
|
47
204
|
raise QueryError(f"Unknown field: {field}")
|
|
48
205
|
|
|
49
|
-
col_prefix = f"{
|
|
206
|
+
col_prefix = f"{target_alias}." if target_alias else ""
|
|
50
207
|
col = f"{col_prefix}{dialect.quote_ident(field)}"
|
|
51
208
|
|
|
52
209
|
if op == "exact":
|
|
@@ -180,10 +337,9 @@ class QuerySet(Generic[TModel]):
|
|
|
180
337
|
cols.append(f"{alias}.{col} AS {col}")
|
|
181
338
|
return cols
|
|
182
339
|
|
|
183
|
-
def _order_by_sql(self) -> str:
|
|
340
|
+
def _order_by_sql(self, *, join_builder: _JoinBuilder) -> str:
|
|
184
341
|
if not self.state.order_by:
|
|
185
342
|
return ""
|
|
186
|
-
alias = _table_alias()
|
|
187
343
|
parts: list[str] = []
|
|
188
344
|
for f in self.state.order_by:
|
|
189
345
|
direction = "ASC"
|
|
@@ -191,19 +347,44 @@ class QuerySet(Generic[TModel]):
|
|
|
191
347
|
if f.startswith("-"):
|
|
192
348
|
direction = "DESC"
|
|
193
349
|
name = f[1:]
|
|
194
|
-
|
|
350
|
+
path = name.split("__")
|
|
351
|
+
rel_path = path[:-1]
|
|
352
|
+
field = path[-1]
|
|
353
|
+
|
|
354
|
+
target_model = self.model
|
|
355
|
+
target_alias = join_builder.base_alias
|
|
356
|
+
if rel_path:
|
|
357
|
+
target_model, target_alias = join_builder.resolve(rel_path)
|
|
358
|
+
|
|
359
|
+
if field not in target_model._meta.fields:
|
|
195
360
|
raise QueryError(f"Unknown order_by field: {name}")
|
|
196
|
-
parts.append(
|
|
361
|
+
parts.append(
|
|
362
|
+
f"{target_alias}.{self.db.dialect.quote_ident(field)} {direction}"
|
|
363
|
+
)
|
|
197
364
|
return " ORDER BY " + ", ".join(parts)
|
|
198
365
|
|
|
199
366
|
def _select_sql(self) -> tuple[str, dict[str, Any]]:
|
|
200
|
-
|
|
367
|
+
join_builder = _JoinBuilder(
|
|
368
|
+
model=self.model, dialect=self.db.dialect, base_alias=_table_alias()
|
|
369
|
+
)
|
|
370
|
+
where_sql, params = _where_from_q(
|
|
371
|
+
self.model,
|
|
372
|
+
self.db.dialect,
|
|
373
|
+
self.state.where,
|
|
374
|
+
table_alias=_table_alias(),
|
|
375
|
+
join_builder=join_builder,
|
|
376
|
+
)
|
|
201
377
|
cols = ", ".join(self._select_columns())
|
|
378
|
+
order_sql = self._order_by_sql(join_builder=join_builder)
|
|
379
|
+
select_kw = "SELECT DISTINCT" if join_builder.needs_distinct else "SELECT"
|
|
202
380
|
sql = (
|
|
203
|
-
f"
|
|
204
|
-
f"{_table_alias()}
|
|
381
|
+
f"{select_kw} {cols} FROM {_qualified_table(self.model, self.db.dialect)} "
|
|
382
|
+
f"{_table_alias()}"
|
|
205
383
|
)
|
|
206
|
-
|
|
384
|
+
if join_builder.joins:
|
|
385
|
+
sql += " " + " ".join(join_builder.joins)
|
|
386
|
+
sql += f" WHERE {where_sql}"
|
|
387
|
+
sql += order_sql
|
|
207
388
|
sql = self.db.dialect.apply_limit_offset(
|
|
208
389
|
sql, self.state.limit, self.state.offset
|
|
209
390
|
)
|
|
@@ -278,12 +459,17 @@ class QuerySet(Generic[TModel]):
|
|
|
278
459
|
set_cols = [k for k in data.keys() if k in self.model._meta.fields]
|
|
279
460
|
if not set_cols:
|
|
280
461
|
return 0
|
|
462
|
+
table = _qualified_table(self.model, self.db.dialect)
|
|
463
|
+
join_builder = _JoinBuilder(
|
|
464
|
+
model=self.model, dialect=self.db.dialect, base_alias=_table_alias()
|
|
465
|
+
)
|
|
281
466
|
where_sql, where_params = _where_from_q(
|
|
282
467
|
self.model,
|
|
283
468
|
self.db.dialect,
|
|
284
469
|
self.state.where,
|
|
285
|
-
table_alias=
|
|
470
|
+
table_alias=_table_alias(),
|
|
286
471
|
param_prefix="w",
|
|
472
|
+
join_builder=join_builder,
|
|
287
473
|
)
|
|
288
474
|
sets = []
|
|
289
475
|
params: dict[str, Any] = {}
|
|
@@ -294,18 +480,38 @@ class QuerySet(Generic[TModel]):
|
|
|
294
480
|
sets.append(f"{self.db.dialect.quote_ident(c)} = :{key}")
|
|
295
481
|
params[key] = data[c]
|
|
296
482
|
params.update(where_params)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
)
|
|
483
|
+
pk = self.model.pk_name()
|
|
484
|
+
outer_pk = f"{table}.{self.db.dialect.quote_ident(pk)}"
|
|
485
|
+
corr = f"{_table_alias()}.{self.db.dialect.quote_ident(pk)} = {outer_pk}"
|
|
486
|
+
sub = f"SELECT 1 FROM {table} {_table_alias()}"
|
|
487
|
+
if join_builder.joins:
|
|
488
|
+
sub += " " + " ".join(join_builder.joins)
|
|
489
|
+
sub += f" WHERE {corr} AND {where_sql}"
|
|
490
|
+
exists_sql = f"EXISTS ({sub})"
|
|
491
|
+
sql = f"UPDATE {table} SET {', '.join(sets)} WHERE {exists_sql}"
|
|
301
492
|
return self.db.execute(sql, params)
|
|
302
493
|
|
|
303
494
|
def delete(self) -> int:
|
|
495
|
+
table = _qualified_table(self.model, self.db.dialect)
|
|
496
|
+
join_builder = _JoinBuilder(
|
|
497
|
+
model=self.model, dialect=self.db.dialect, base_alias=_table_alias()
|
|
498
|
+
)
|
|
304
499
|
where_sql, params = _where_from_q(
|
|
305
|
-
self.model,
|
|
500
|
+
self.model,
|
|
501
|
+
self.db.dialect,
|
|
502
|
+
self.state.where,
|
|
503
|
+
table_alias=_table_alias(),
|
|
504
|
+
join_builder=join_builder,
|
|
306
505
|
)
|
|
307
|
-
|
|
308
|
-
|
|
506
|
+
pk = self.model.pk_name()
|
|
507
|
+
outer_pk = f"{table}.{self.db.dialect.quote_ident(pk)}"
|
|
508
|
+
corr = f"{_table_alias()}.{self.db.dialect.quote_ident(pk)} = {outer_pk}"
|
|
509
|
+
sub = f"SELECT 1 FROM {table} {_table_alias()}"
|
|
510
|
+
if join_builder.joins:
|
|
511
|
+
sub += " " + " ".join(join_builder.joins)
|
|
512
|
+
sub += f" WHERE {corr} AND {where_sql}"
|
|
513
|
+
exists_sql = f"EXISTS ({sub})"
|
|
514
|
+
sql = f"DELETE FROM {table} WHERE {exists_sql}"
|
|
309
515
|
return self.db.execute(sql, params)
|
|
310
516
|
|
|
311
517
|
def _prefetch(self, instances: list[Any]) -> None:
|
|
@@ -458,10 +664,9 @@ class AsyncQuerySet(Generic[TModel]):
|
|
|
458
664
|
cols.append(f"{alias}.{col} AS {col}")
|
|
459
665
|
return cols
|
|
460
666
|
|
|
461
|
-
def _order_by_sql(self) -> str:
|
|
667
|
+
def _order_by_sql(self, *, join_builder: _JoinBuilder) -> str:
|
|
462
668
|
if not self.state.order_by:
|
|
463
669
|
return ""
|
|
464
|
-
alias = _table_alias()
|
|
465
670
|
parts: list[str] = []
|
|
466
671
|
for f in self.state.order_by:
|
|
467
672
|
direction = "ASC"
|
|
@@ -469,19 +674,44 @@ class AsyncQuerySet(Generic[TModel]):
|
|
|
469
674
|
if f.startswith("-"):
|
|
470
675
|
direction = "DESC"
|
|
471
676
|
name = f[1:]
|
|
472
|
-
|
|
677
|
+
path = name.split("__")
|
|
678
|
+
rel_path = path[:-1]
|
|
679
|
+
field = path[-1]
|
|
680
|
+
|
|
681
|
+
target_model = self.model
|
|
682
|
+
target_alias = join_builder.base_alias
|
|
683
|
+
if rel_path:
|
|
684
|
+
target_model, target_alias = join_builder.resolve(rel_path)
|
|
685
|
+
|
|
686
|
+
if field not in target_model._meta.fields:
|
|
473
687
|
raise QueryError(f"Unknown order_by field: {name}")
|
|
474
|
-
parts.append(
|
|
688
|
+
parts.append(
|
|
689
|
+
f"{target_alias}.{self.db.dialect.quote_ident(field)} {direction}"
|
|
690
|
+
)
|
|
475
691
|
return " ORDER BY " + ", ".join(parts)
|
|
476
692
|
|
|
477
693
|
def _select_sql(self) -> tuple[str, dict[str, Any]]:
|
|
478
|
-
|
|
694
|
+
join_builder = _JoinBuilder(
|
|
695
|
+
model=self.model, dialect=self.db.dialect, base_alias=_table_alias()
|
|
696
|
+
)
|
|
697
|
+
where_sql, params = _where_from_q(
|
|
698
|
+
self.model,
|
|
699
|
+
self.db.dialect,
|
|
700
|
+
self.state.where,
|
|
701
|
+
table_alias=_table_alias(),
|
|
702
|
+
join_builder=join_builder,
|
|
703
|
+
)
|
|
479
704
|
cols = ", ".join(self._select_columns())
|
|
705
|
+
order_sql = self._order_by_sql(join_builder=join_builder)
|
|
706
|
+
select_kw = "SELECT DISTINCT" if join_builder.needs_distinct else "SELECT"
|
|
480
707
|
sql = (
|
|
481
|
-
f"
|
|
482
|
-
f"{_table_alias()}
|
|
708
|
+
f"{select_kw} {cols} FROM {_qualified_table(self.model, self.db.dialect)} "
|
|
709
|
+
f"{_table_alias()}"
|
|
483
710
|
)
|
|
484
|
-
|
|
711
|
+
if join_builder.joins:
|
|
712
|
+
sql += " " + " ".join(join_builder.joins)
|
|
713
|
+
sql += f" WHERE {where_sql}"
|
|
714
|
+
sql += order_sql
|
|
485
715
|
sql = self.db.dialect.apply_limit_offset(
|
|
486
716
|
sql, self.state.limit, self.state.offset
|
|
487
717
|
)
|
|
@@ -556,12 +786,17 @@ class AsyncQuerySet(Generic[TModel]):
|
|
|
556
786
|
set_cols = [k for k in data.keys() if k in self.model._meta.fields]
|
|
557
787
|
if not set_cols:
|
|
558
788
|
return 0
|
|
789
|
+
table = _qualified_table(self.model, self.db.dialect)
|
|
790
|
+
join_builder = _JoinBuilder(
|
|
791
|
+
model=self.model, dialect=self.db.dialect, base_alias=_table_alias()
|
|
792
|
+
)
|
|
559
793
|
where_sql, where_params = _where_from_q(
|
|
560
794
|
self.model,
|
|
561
795
|
self.db.dialect,
|
|
562
796
|
self.state.where,
|
|
563
|
-
table_alias=
|
|
797
|
+
table_alias=_table_alias(),
|
|
564
798
|
param_prefix="w",
|
|
799
|
+
join_builder=join_builder,
|
|
565
800
|
)
|
|
566
801
|
sets = []
|
|
567
802
|
params: dict[str, Any] = {}
|
|
@@ -572,18 +807,38 @@ class AsyncQuerySet(Generic[TModel]):
|
|
|
572
807
|
sets.append(f"{self.db.dialect.quote_ident(c)} = :{key}")
|
|
573
808
|
params[key] = data[c]
|
|
574
809
|
params.update(where_params)
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
)
|
|
810
|
+
pk = self.model.pk_name()
|
|
811
|
+
outer_pk = f"{table}.{self.db.dialect.quote_ident(pk)}"
|
|
812
|
+
corr = f"{_table_alias()}.{self.db.dialect.quote_ident(pk)} = {outer_pk}"
|
|
813
|
+
sub = f"SELECT 1 FROM {table} {_table_alias()}"
|
|
814
|
+
if join_builder.joins:
|
|
815
|
+
sub += " " + " ".join(join_builder.joins)
|
|
816
|
+
sub += f" WHERE {corr} AND {where_sql}"
|
|
817
|
+
exists_sql = f"EXISTS ({sub})"
|
|
818
|
+
sql = f"UPDATE {table} SET {', '.join(sets)} WHERE {exists_sql}"
|
|
579
819
|
return await self.db.execute(sql, params)
|
|
580
820
|
|
|
581
821
|
async def delete(self) -> int:
|
|
822
|
+
table = _qualified_table(self.model, self.db.dialect)
|
|
823
|
+
join_builder = _JoinBuilder(
|
|
824
|
+
model=self.model, dialect=self.db.dialect, base_alias=_table_alias()
|
|
825
|
+
)
|
|
582
826
|
where_sql, params = _where_from_q(
|
|
583
|
-
self.model,
|
|
827
|
+
self.model,
|
|
828
|
+
self.db.dialect,
|
|
829
|
+
self.state.where,
|
|
830
|
+
table_alias=_table_alias(),
|
|
831
|
+
join_builder=join_builder,
|
|
584
832
|
)
|
|
585
|
-
|
|
586
|
-
|
|
833
|
+
pk = self.model.pk_name()
|
|
834
|
+
outer_pk = f"{table}.{self.db.dialect.quote_ident(pk)}"
|
|
835
|
+
corr = f"{_table_alias()}.{self.db.dialect.quote_ident(pk)} = {outer_pk}"
|
|
836
|
+
sub = f"SELECT 1 FROM {table} {_table_alias()}"
|
|
837
|
+
if join_builder.joins:
|
|
838
|
+
sub += " " + " ".join(join_builder.joins)
|
|
839
|
+
sub += f" WHERE {corr} AND {where_sql}"
|
|
840
|
+
exists_sql = f"EXISTS ({sub})"
|
|
841
|
+
sql = f"DELETE FROM {table} WHERE {exists_sql}"
|
|
587
842
|
return await self.db.execute(sql, params)
|
|
588
843
|
|
|
589
844
|
async def _prefetch(self, instances: list[Any]) -> None:
|
|
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
|
|
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
|