half-orm 0.18.8__tar.gz → 0.18.10__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.8/half_orm.egg-info → half_orm-0.18.10}/PKG-INFO +1 -1
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/fkey.py +29 -17
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/relation.py +42 -6
- half_orm-0.18.10/half_orm/version.txt +1 -0
- {half_orm-0.18.8 → half_orm-0.18.10/half_orm.egg-info}/PKG-INFO +1 -1
- half_orm-0.18.8/half_orm/version.txt +0 -1
- {half_orm-0.18.8 → half_orm-0.18.10}/AUTHORS +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/LICENSE +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/README.md +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/__init__.py +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/__main__.py +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/cli.py +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/cli_utils.py +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/field.py +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/field_errors.py +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/hotest.py +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/model.py +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/model_errors.py +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/null.py +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/pg_meta.py +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/relation_errors.py +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/relation_factory.py +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/sql_adapter.py +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/sql_ast.py +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/transaction.py +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm/utils.py +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm.egg-info/SOURCES.txt +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm.egg-info/dependency_links.txt +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm.egg-info/entry_points.txt +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm.egg-info/requires.txt +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/half_orm.egg-info/top_level.txt +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/pyproject.toml +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/setup.cfg +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/setup.py +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/test/test_cli.py +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/test/test_main.py +0 -0
- {half_orm-0.18.8 → half_orm-0.18.10}/test/test_sql_ast.py +0 -0
|
@@ -119,40 +119,48 @@ class FKey:
|
|
|
119
119
|
def values(self):
|
|
120
120
|
return [list(elt.values()) for elt in self.__to_relation.ho_select(*self.__fk_names)]
|
|
121
121
|
|
|
122
|
-
def set(self,
|
|
122
|
+
def set(self, ho_to_rel=None, **kwargs):
|
|
123
123
|
"""Bind this foreign key to a relation, adding a JOIN condition.
|
|
124
124
|
|
|
125
|
-
After calling ``.set(
|
|
126
|
-
automatically include a JOIN against
|
|
127
|
-
|
|
125
|
+
After calling ``.set(...)``, queries on the owning relation
|
|
126
|
+
automatically include a JOIN against the related table filtered by
|
|
127
|
+
the given constraints.
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
``.set(
|
|
129
|
+
Three calling forms — no import of the related class required:
|
|
130
|
+
|
|
131
|
+
- ``.set()`` — joins all rows (tautological predicate).
|
|
132
|
+
- ``.set(field=value, ...)`` — joins with constraints, instantiating
|
|
133
|
+
the related class internally.
|
|
134
|
+
- ``.set(rel)`` — joins against an already-built
|
|
135
|
+
:class:`~half_orm.relation.Relation` instance.
|
|
132
136
|
|
|
133
137
|
Args:
|
|
134
|
-
|
|
135
|
-
|
|
138
|
+
ho_to_rel (Relation | None): an existing relation to join against.
|
|
139
|
+
Mutually exclusive with ``**kwargs``.
|
|
140
|
+
**kwargs: field constraints forwarded to the related class
|
|
141
|
+
constructor. Ignored when ``ho_to_rel`` is provided.
|
|
136
142
|
|
|
137
143
|
Returns:
|
|
138
144
|
self — for chaining.
|
|
139
145
|
|
|
140
146
|
Raises:
|
|
141
|
-
RuntimeError: if ``
|
|
147
|
+
RuntimeError: if ``ho_to_rel`` is not a :class:`~half_orm.relation.Relation` instance.
|
|
148
|
+
RuntimeError: if ``ho_to_rel`` and ``**kwargs`` are both provided.
|
|
142
149
|
RuntimeError: if setting this FK would create a cycle in the join graph.
|
|
143
150
|
|
|
144
151
|
Example:
|
|
145
|
-
|
|
152
|
+
Without importing the related class:
|
|
146
153
|
```python
|
|
147
154
|
post = Post()
|
|
148
|
-
post.author_fk.set(
|
|
149
|
-
|
|
155
|
+
post.author_fk.set() # all authors
|
|
156
|
+
post.author_fk.set(last_name=('like', 'Mar%')) # filtered
|
|
150
157
|
```
|
|
151
158
|
|
|
152
|
-
|
|
159
|
+
With an existing relation:
|
|
153
160
|
```python
|
|
154
161
|
post = Post()
|
|
155
|
-
post.author_fk.set(
|
|
162
|
+
post.author_fk.set(Author(last_name=('like', 'Mar%')))
|
|
163
|
+
# equivalent to: Author(last_name=('like', 'Mar%')).post_rfk().ho_count()
|
|
156
164
|
```
|
|
157
165
|
|
|
158
166
|
*New in version 0.18.6:* raises ``RuntimeError`` if setting this FK would create a cycle in the join graph.
|
|
@@ -160,8 +168,12 @@ class FKey:
|
|
|
160
168
|
# pylint: disable=import-outside-toplevel
|
|
161
169
|
from half_orm.relation import Relation
|
|
162
170
|
|
|
163
|
-
if
|
|
164
|
-
|
|
171
|
+
if ho_to_rel is not None and kwargs:
|
|
172
|
+
raise RuntimeError("FKey.set: cannot pass both a Relation and keyword arguments")
|
|
173
|
+
if ho_to_rel is None:
|
|
174
|
+
__to = self.__get_rel(normalize_qrn(self.__fk_fqrn))(**kwargs)
|
|
175
|
+
else:
|
|
176
|
+
__to = ho_to_rel
|
|
165
177
|
if not issubclass(__to.__class__, Relation):
|
|
166
178
|
raise RuntimeError("Fkey.set excepts an argument of type Relation")
|
|
167
179
|
from_ = self.__relation
|
|
@@ -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.
|
|
@@ -465,6 +471,22 @@ class Relation:
|
|
|
465
471
|
for ukey in self._ho_ukeys:
|
|
466
472
|
if _fully_set(ukey):
|
|
467
473
|
return self
|
|
474
|
+
# A FK join uniquely identifies self when:
|
|
475
|
+
# - the fields on self involved in the join form a PK or UNIQUE NOT NULL, AND
|
|
476
|
+
# - the corresponding fields on the joined relation are all fixed with '='.
|
|
477
|
+
def _all_eq(names, rel):
|
|
478
|
+
return all(
|
|
479
|
+
rel._ho_fields.get(n.strip('"')) is not None
|
|
480
|
+
and rel._ho_fields[n.strip('"')].is_set()
|
|
481
|
+
and rel._ho_fields[n.strip('"')]._comp() == '='
|
|
482
|
+
for n in names
|
|
483
|
+
)
|
|
484
|
+
for fkey, fk_rel in self._ho_join_to.items():
|
|
485
|
+
self_fields = frozenset(fkey.names)
|
|
486
|
+
on_pk = bool(self._ho_pkey) and self_fields == frozenset(self._ho_pkey.keys())
|
|
487
|
+
on_ukey = any(self_fields == frozenset(uk.keys()) for uk in self._ho_ukeys)
|
|
488
|
+
if (on_pk or on_ukey) and _all_eq(fkey.fk_names, fk_rel):
|
|
489
|
+
return self
|
|
468
490
|
if not self._ho_pkey and not self._ho_ukeys:
|
|
469
491
|
raise relation_errors.NotASingletonError(
|
|
470
492
|
f"{self.__class__.__name__} has no primary key or unique NOT NULL constraint.")
|
|
@@ -896,10 +918,24 @@ Fkeys = {"""
|
|
|
896
918
|
def ho_is_set(self):
|
|
897
919
|
"""Return True if one field at least is set or if self has been
|
|
898
920
|
constrained by at least one of its foreign keys or self is the
|
|
899
|
-
result of a combination of Relations (using set operators)
|
|
921
|
+
result of a combination of Relations (using set operators) where
|
|
922
|
+
at least one operand is itself constrained.
|
|
900
923
|
"""
|
|
901
|
-
joined_to =
|
|
902
|
-
|
|
924
|
+
joined_to = any(jt_.ho_is_set() for jt_ in self._ho_join_to.values())
|
|
925
|
+
op = self._ho_set_operators.operator
|
|
926
|
+
if op:
|
|
927
|
+
left_set = self._ho_set_operators.left.ho_is_set()
|
|
928
|
+
right = self._ho_set_operators.right
|
|
929
|
+
right_set = right is not None and right.ho_is_set()
|
|
930
|
+
if op == "or":
|
|
931
|
+
# A() | B → all rows if any operand is unconstrained
|
|
932
|
+
set_op_constrained = left_set and right_set
|
|
933
|
+
else:
|
|
934
|
+
# "and" / "and not": unconstrained operand is transparent
|
|
935
|
+
set_op_constrained = left_set or right_set
|
|
936
|
+
else:
|
|
937
|
+
set_op_constrained = False
|
|
938
|
+
return (joined_to or set_op_constrained or bool(self._ho_neg) or
|
|
903
939
|
bool({field for field in self._ho_fields.values() if field.is_set()}))
|
|
904
940
|
|
|
905
941
|
def __get_set_fields(self):
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.18.10
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.18.8
|
|
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
|