half-orm 0.18.9__tar.gz → 0.18.11__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.
- {half_orm-0.18.9/half_orm.egg-info → half_orm-0.18.11}/PKG-INFO +1 -1
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm/relation.py +100 -25
- half_orm-0.18.11/half_orm/version.txt +1 -0
- {half_orm-0.18.9 → half_orm-0.18.11/half_orm.egg-info}/PKG-INFO +1 -1
- half_orm-0.18.9/half_orm/version.txt +0 -1
- {half_orm-0.18.9 → half_orm-0.18.11}/AUTHORS +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/LICENSE +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/README.md +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm/__init__.py +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm/__main__.py +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm/cli.py +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm/cli_utils.py +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm/field.py +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm/field_errors.py +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm/fkey.py +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm/hotest.py +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm/model.py +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm/model_errors.py +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm/null.py +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm/pg_meta.py +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm/relation_errors.py +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm/relation_factory.py +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm/sql_adapter.py +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm/sql_ast.py +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm/transaction.py +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm/utils.py +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm.egg-info/SOURCES.txt +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm.egg-info/dependency_links.txt +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm.egg-info/entry_points.txt +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm.egg-info/requires.txt +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/half_orm.egg-info/top_level.txt +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/pyproject.toml +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/setup.cfg +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/setup.py +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/test/test_cli.py +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/test/test_main.py +0 -0
- {half_orm-0.18.9 → half_orm-0.18.11}/test/test_sql_ast.py +0 -0
|
@@ -429,9 +429,15 @@ class Relation:
|
|
|
429
429
|
def ho_assert_is_singleton(self):
|
|
430
430
|
"""Assert that this predicate identifies exactly one row, without querying the database.
|
|
431
431
|
|
|
432
|
-
A predicate is a *singleton* when
|
|
433
|
-
|
|
434
|
-
|
|
432
|
+
A predicate is a *singleton* when:
|
|
433
|
+
|
|
434
|
+
* every field of a unique identifier (primary key or any
|
|
435
|
+
``UNIQUE NOT NULL`` constraint) is set with the ``=`` comparator, **or**
|
|
436
|
+
* a FK join constrains a unique identifier of this relation: the fields
|
|
437
|
+
on *this* side of the join form a PK or UNIQUE NOT NULL, and the
|
|
438
|
+
corresponding fields on the joined relation are all fixed with ``=``.
|
|
439
|
+
|
|
440
|
+
The check is purely structural — no SQL is executed.
|
|
435
441
|
|
|
436
442
|
Returns:
|
|
437
443
|
self — for chaining before a write operation.
|
|
@@ -451,8 +457,14 @@ class Relation:
|
|
|
451
457
|
# Raises — last_name is not a unique identifier
|
|
452
458
|
Author(last_name='Martin').ho_assert_is_singleton()
|
|
453
459
|
|
|
460
|
+
# OK — FK navigation: comment.post_id fixes post.id (PK)
|
|
461
|
+
Comment(post_id=42).fk_post().ho_assert_is_singleton()
|
|
462
|
+
|
|
454
463
|
# Typical usage: guard a single-row write
|
|
455
464
|
Author(id=42).ho_assert_is_singleton().ho_update(email='new@example.com')
|
|
465
|
+
|
|
466
|
+
# Via FK navigation: delete the post linked to a specific comment
|
|
467
|
+
Comment(post_id=42).fk_post().ho_assert_is_singleton().ho_delete()
|
|
456
468
|
```
|
|
457
469
|
|
|
458
470
|
*New in version 0.18.0.*
|
|
@@ -465,6 +477,22 @@ class Relation:
|
|
|
465
477
|
for ukey in self._ho_ukeys:
|
|
466
478
|
if _fully_set(ukey):
|
|
467
479
|
return self
|
|
480
|
+
# A FK join uniquely identifies self when:
|
|
481
|
+
# - the fields on self involved in the join form a PK or UNIQUE NOT NULL, AND
|
|
482
|
+
# - the corresponding fields on the joined relation are all fixed with '='.
|
|
483
|
+
def _all_eq(names, rel):
|
|
484
|
+
return all(
|
|
485
|
+
rel._ho_fields.get(n.strip('"')) is not None
|
|
486
|
+
and rel._ho_fields[n.strip('"')].is_set()
|
|
487
|
+
and rel._ho_fields[n.strip('"')]._comp() == '='
|
|
488
|
+
for n in names
|
|
489
|
+
)
|
|
490
|
+
for fkey, fk_rel in self._ho_join_to.items():
|
|
491
|
+
self_fields = frozenset(fkey.names)
|
|
492
|
+
on_pk = bool(self._ho_pkey) and self_fields == frozenset(self._ho_pkey.keys())
|
|
493
|
+
on_ukey = any(self_fields == frozenset(uk.keys()) for uk in self._ho_ukeys)
|
|
494
|
+
if (on_pk or on_ukey) and _all_eq(fkey.fk_names, fk_rel):
|
|
495
|
+
return self
|
|
468
496
|
if not self._ho_pkey and not self._ho_ukeys:
|
|
469
497
|
raise relation_errors.NotASingletonError(
|
|
470
498
|
f"{self.__class__.__name__} has no primary key or unique NOT NULL constraint.")
|
|
@@ -532,20 +560,37 @@ class Relation:
|
|
|
532
560
|
if not update_args:
|
|
533
561
|
return None
|
|
534
562
|
self._ho_query_type = 'update'
|
|
535
|
-
_, where_expr = self.__where_args()
|
|
536
563
|
set_clause = []
|
|
537
564
|
for field_name, new_value in update_args.items():
|
|
538
565
|
col_name = self._ho_fields[field_name].name
|
|
539
566
|
set_clause.append((f'"{col_name}" = %s', new_value))
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
567
|
+
if self._ho_join_to:
|
|
568
|
+
# Build a subquery to avoid any DB round-trip and to correctly
|
|
569
|
+
# propagate JOIN constraints into the UPDATE predicate.
|
|
570
|
+
pk_names = list(self._ho_pkey.keys())
|
|
571
|
+
sub_sql, sub_vals = self._ho_prep_select(*pk_names)
|
|
572
|
+
if len(pk_names) == 1:
|
|
573
|
+
where = ASTRaw(f'"{pk_names[0]}" in ({sub_sql})', sub_vals)
|
|
574
|
+
else:
|
|
575
|
+
pk_cols = ', '.join(f'"{pk}"' for pk in pk_names)
|
|
576
|
+
where = ASTRaw(f'({pk_cols}) in ({sub_sql})', sub_vals)
|
|
577
|
+
stmt = ASTUpdate(
|
|
578
|
+
table=self._qrn,
|
|
579
|
+
set_clause=set_clause,
|
|
580
|
+
where=where,
|
|
581
|
+
returning=ASTReturning(list(args)) if args else None,
|
|
582
|
+
)
|
|
583
|
+
else:
|
|
584
|
+
_, where_expr = self.__where_args()
|
|
585
|
+
fk_where_str, fk_values = self.__fkey_where('', [])
|
|
586
|
+
stmt = ASTUpdate(
|
|
587
|
+
table=self._qrn,
|
|
588
|
+
set_clause=set_clause,
|
|
589
|
+
where=where_expr,
|
|
590
|
+
fk_where=fk_where_str or None,
|
|
591
|
+
fk_values=fk_values,
|
|
592
|
+
returning=ASTReturning(list(args)) if args else None,
|
|
593
|
+
)
|
|
549
594
|
query, vals = stmt.to_sql()
|
|
550
595
|
return query, tuple(vals), update_args
|
|
551
596
|
|
|
@@ -599,15 +644,31 @@ class Relation:
|
|
|
599
644
|
f'Attempt to delete all rows from {self.__class__.__name__}'
|
|
600
645
|
' without delete_all being set to True!')
|
|
601
646
|
self._ho_query_type = 'delete'
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
647
|
+
if self._ho_join_to:
|
|
648
|
+
# Build a subquery to avoid any DB round-trip and to correctly
|
|
649
|
+
# propagate JOIN constraints into the DELETE predicate.
|
|
650
|
+
pk_names = list(self._ho_pkey.keys())
|
|
651
|
+
sub_sql, sub_vals = self._ho_prep_select(*pk_names)
|
|
652
|
+
if len(pk_names) == 1:
|
|
653
|
+
where = ASTRaw(f'"{pk_names[0]}" in ({sub_sql})', sub_vals)
|
|
654
|
+
else:
|
|
655
|
+
pk_cols = ', '.join(f'"{pk}"' for pk in pk_names)
|
|
656
|
+
where = ASTRaw(f'({pk_cols}) in ({sub_sql})', sub_vals)
|
|
657
|
+
stmt = ASTDelete(
|
|
658
|
+
table=self._qrn,
|
|
659
|
+
where=where,
|
|
660
|
+
returning=ASTReturning(list(args)) if args else None,
|
|
661
|
+
)
|
|
662
|
+
else:
|
|
663
|
+
_, where_expr = self.__where_args()
|
|
664
|
+
fk_where_str, fk_values = self.__fkey_where('', [])
|
|
665
|
+
stmt = ASTDelete(
|
|
666
|
+
table=self._qrn,
|
|
667
|
+
where=where_expr,
|
|
668
|
+
fk_where=fk_where_str or None,
|
|
669
|
+
fk_values=fk_values,
|
|
670
|
+
returning=ASTReturning(list(args)) if args else None,
|
|
671
|
+
)
|
|
611
672
|
query, vals = stmt.to_sql()
|
|
612
673
|
return query, tuple(vals)
|
|
613
674
|
|
|
@@ -896,10 +957,24 @@ Fkeys = {"""
|
|
|
896
957
|
def ho_is_set(self):
|
|
897
958
|
"""Return True if one field at least is set or if self has been
|
|
898
959
|
constrained by at least one of its foreign keys or self is the
|
|
899
|
-
result of a combination of Relations (using set operators)
|
|
960
|
+
result of a combination of Relations (using set operators) where
|
|
961
|
+
at least one operand is itself constrained.
|
|
900
962
|
"""
|
|
901
|
-
joined_to =
|
|
902
|
-
|
|
963
|
+
joined_to = any(jt_.ho_is_set() for jt_ in self._ho_join_to.values())
|
|
964
|
+
op = self._ho_set_operators.operator
|
|
965
|
+
if op:
|
|
966
|
+
left_set = self._ho_set_operators.left.ho_is_set()
|
|
967
|
+
right = self._ho_set_operators.right
|
|
968
|
+
right_set = right is not None and right.ho_is_set()
|
|
969
|
+
if op == "or":
|
|
970
|
+
# A() | B → all rows if any operand is unconstrained
|
|
971
|
+
set_op_constrained = left_set and right_set
|
|
972
|
+
else:
|
|
973
|
+
# "and" / "and not": unconstrained operand is transparent
|
|
974
|
+
set_op_constrained = left_set or right_set
|
|
975
|
+
else:
|
|
976
|
+
set_op_constrained = False
|
|
977
|
+
return (joined_to or set_op_constrained or bool(self._ho_neg) or
|
|
903
978
|
bool({field for field in self._ho_fields.values() if field.is_set()}))
|
|
904
979
|
|
|
905
980
|
def __get_set_fields(self):
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.18.11
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.18.9
|
|
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
|