ns-orm 0.0.2__tar.gz → 0.0.3__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.
Files changed (42) hide show
  1. {ns_orm-0.0.2/src/ns_orm.egg-info → ns_orm-0.0.3}/PKG-INFO +1 -1
  2. {ns_orm-0.0.2 → ns_orm-0.0.3}/pyproject.toml +1 -1
  3. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm/manager.py +6 -0
  4. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm/query.py +293 -38
  5. {ns_orm-0.0.2 → ns_orm-0.0.3/src/ns_orm.egg-info}/PKG-INFO +1 -1
  6. {ns_orm-0.0.2 → ns_orm-0.0.3}/LICENSE +0 -0
  7. {ns_orm-0.0.2 → ns_orm-0.0.3}/MANIFEST.in +0 -0
  8. {ns_orm-0.0.2 → ns_orm-0.0.3}/README.md +0 -0
  9. {ns_orm-0.0.2 → ns_orm-0.0.3}/doc/async.md +0 -0
  10. {ns_orm-0.0.2 → ns_orm-0.0.3}/doc/database.md +0 -0
  11. {ns_orm-0.0.2 → ns_orm-0.0.3}/doc/index.md +0 -0
  12. {ns_orm-0.0.2 → ns_orm-0.0.3}/doc/migrations.md +0 -0
  13. {ns_orm-0.0.2 → ns_orm-0.0.3}/doc/models.md +0 -0
  14. {ns_orm-0.0.2 → ns_orm-0.0.3}/doc/queryset.md +0 -0
  15. {ns_orm-0.0.2 → ns_orm-0.0.3}/doc/relations.md +0 -0
  16. {ns_orm-0.0.2 → ns_orm-0.0.3}/doc/release.md +0 -0
  17. {ns_orm-0.0.2 → ns_orm-0.0.3}/doc/schema.md +0 -0
  18. {ns_orm-0.0.2 → ns_orm-0.0.3}/setup.cfg +0 -0
  19. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm/__init__.py +0 -0
  20. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm/cli.py +0 -0
  21. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm/database.py +0 -0
  22. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm/dialects.py +0 -0
  23. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm/exceptions.py +0 -0
  24. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm/expressions.py +0 -0
  25. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm/fields.py +0 -0
  26. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm/migrations/__init__.py +0 -0
  27. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm/migrations/autodetector.py +0 -0
  28. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm/migrations/executor.py +0 -0
  29. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm/migrations/loader.py +0 -0
  30. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm/migrations/migration.py +0 -0
  31. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm/migrations/operations.py +0 -0
  32. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm/migrations/state.py +0 -0
  33. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm/migrations/writer.py +0 -0
  34. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm/model.py +0 -0
  35. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm/schema.py +0 -0
  36. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm/typing.py +0 -0
  37. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm/utils.py +0 -0
  38. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm.egg-info/SOURCES.txt +0 -0
  39. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm.egg-info/dependency_links.txt +0 -0
  40. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm.egg-info/entry_points.txt +0 -0
  41. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm.egg-info/requires.txt +0 -0
  42. {ns_orm-0.0.2 → ns_orm-0.0.3}/src/ns_orm.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ns-orm
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: A ORM built on Pydantic models.
5
5
  Author: ns-orm contributors
6
6
  License: Apache License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ns-orm"
7
- version = "0.0.2"
7
+ version = "0.0.3"
8
8
  description = "A ORM built on Pydantic models."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.7"
@@ -59,6 +59,12 @@ class Manager(Generic[TModel]):
59
59
  return self.aqs().exclude(**lookups)
60
60
  return self.qs().exclude(**lookups)
61
61
 
62
+ def order_by(self, *fields: str) -> Any:
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
  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
- parts = lookup.split("__")
44
- field = parts[0]
45
- op = parts[1] if len(parts) > 1 else "exact"
46
- if field not in model._meta.fields:
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"{table_alias}." if table_alias else ""
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
- if name not in self.model._meta.fields:
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(f"{alias}.{self.db.dialect.quote_ident(name)} {direction}")
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
- where_sql, params = _where_from_q(self.model, self.db.dialect, self.state.where)
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"SELECT {cols} FROM {_qualified_table(self.model, self.db.dialect)} "
204
- f"{_table_alias()} WHERE {where_sql}"
381
+ f"{select_kw} {cols} FROM {_qualified_table(self.model, self.db.dialect)} "
382
+ f"{_table_alias()}"
205
383
  )
206
- sql += self._order_by_sql()
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
- sql = (
298
- f"UPDATE {_qualified_table(self.model, self.db.dialect)} "
299
- f"SET {', '.join(sets)} WHERE {where_sql}"
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, self.db.dialect, self.state.where, table_alias=""
500
+ self.model,
501
+ self.db.dialect,
502
+ self.state.where,
503
+ table_alias=_table_alias(),
504
+ join_builder=join_builder,
306
505
  )
307
- table = _qualified_table(self.model, self.db.dialect)
308
- sql = f"DELETE FROM {table} WHERE {where_sql}"
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
- if name not in self.model._meta.fields:
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(f"{alias}.{self.db.dialect.quote_ident(name)} {direction}")
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
- where_sql, params = _where_from_q(self.model, self.db.dialect, self.state.where)
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"SELECT {cols} FROM {_qualified_table(self.model, self.db.dialect)} "
482
- f"{_table_alias()} WHERE {where_sql}"
708
+ f"{select_kw} {cols} FROM {_qualified_table(self.model, self.db.dialect)} "
709
+ f"{_table_alias()}"
483
710
  )
484
- sql += self._order_by_sql()
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
- sql = (
576
- f"UPDATE {_qualified_table(self.model, self.db.dialect)} "
577
- f"SET {', '.join(sets)} WHERE {where_sql}"
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, self.db.dialect, self.state.where, table_alias=""
827
+ self.model,
828
+ self.db.dialect,
829
+ self.state.where,
830
+ table_alias=_table_alias(),
831
+ join_builder=join_builder,
584
832
  )
585
- table = _qualified_table(self.model, self.db.dialect)
586
- sql = f"DELETE FROM {table} WHERE {where_sql}"
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ns-orm
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: A ORM built on Pydantic models.
5
5
  Author: ns-orm contributors
6
6
  License: Apache License
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