Relation¶
A Relation object is a predicate — it describes the logical condition
that rows must satisfy to belong to the relation. Its extension is the set
of rows currently satisfying that predicate in the database.
Instantiating with keyword arguments specialises the predicate. No SQL is executed until you call an executor method.
Author() # tautology — the whole table
Author(last_name='Martin') # subset: "is an author named Martin"
Author(last_name='Martin', id=42) # at most one row
Executors (ho_select, ho_insert, ho_update, ho_delete, ho_count,
ho_is_empty) execute SQL immediately and return results.
Introspection methods (ho_assert_is_singleton, ho_is_set,
ho_where_display, ho_mogrify) inspect or assert on the predicate without
touching the database.
See Learn halfORM in half an hour for a full walkthrough.
Executors¶
These methods execute SQL immediately and return results.
ho_insert(*args)
¶
Insert the row described by this predicate. Executes SQL.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*args
|
column names to include in the returned dict. If omitted,
all columns are returned (equivalent to |
()
|
Returns:
| Name | Type | Description |
|---|---|---|
dict |
[dict]
|
the inserted row. |
Raises:
| Type | Description |
|---|---|
ReadOnlyRelationError
|
if the relation is a view or other non-writable kind. |
ho_select(*args, distinct=False, order_by=None, limit=None, offset=None, json_agg=None)
¶
Enumerate the extension of this predicate. Executes SQL.
This method is a generator. Without arguments it is equivalent to
iterating directly on the relation object (for row in rel:).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*args
|
column names to project. If omitted, all columns are returned. |
()
|
|
distinct
|
bool
|
add |
False
|
order_by
|
str
|
SQL |
None
|
limit
|
int
|
maximum number of rows to return. Default: |
None
|
offset
|
int
|
number of rows to skip. Default: |
None
|
json_agg
|
dict
|
aggregate already-set fkeys as JSON arrays via
a Each entry maps a fkey attribute name to its spec:
The fkey must have been set via The type of the aggregated value depends on the FK direction:
|
None
|
Yields:
| Name | Type | Description |
|---|---|---|
dict |
one row of the extension. |
Example
Project and sort:
for row in Author(last_name='Martin').ho_select('id', 'email', order_by='id'):
print(row) # {'id': 1, 'email': 'alice@example.com'}
Aggregate related rows as JSON (reverse FK):
alice = Author(last_name='Martin')
alice.post_rfk.set() # join all posts
for row in alice.ho_select(json_agg={'post_rfk': ['id', 'title']}):
print(row['post_rfk']) # [{'id': 1, 'title': '...'}, ...]
Chained FK (A ← B → C) — aggregate the leaf relation's data:
# For each post, collect the persons who commented on it.
# post ← comment → person (comment is the junction)
post = Post(title='Hello')
comment = Comment()
comment.author_fk.set() # chain: comment → person
post.comment_rfk.set(comment)
for row in post.ho_select(json_agg={'comment_rfk': ['last_name']}):
print(row['comment_rfk']) # [{'last_name': '...'}, ...]
New in version 0.18.0: distinct, order_by, limit and offset parameters.
New in version 0.18.6: json_agg parameter.
Changed in version 0.18.7 (breaking): direct FK and singleton reverse FK (UNIQUE/PK) in json_agg return a dict (or None) instead of a list.
json_agg: FK direction determines the return type
When using the json_agg parameter, the type of each aggregated value
depends on the FK direction:
| FK type | Condition | Return value |
|---|---|---|
| Reverse (one-to-many) | no UNIQUE/PK on FK columns | list of dicts ([] if empty) |
| Reverse (one-to-one) | FK columns have UNIQUE or PK | dict or None |
| Direct (many-to-one) | — | dict or None |
# Reverse FK, non-unique — author → posts (list)
alice = Author(last_name='Martin')
alice.post_rfk.set() # join all posts
for row in alice.ho_select(json_agg={'post_rfk': ['title']}):
print(row['post_rfk']) # [{'title': '...'}, ...]
# Direct FK — post → author (dict)
post = Post(title='Hello')
post.author_fk.set() # join all authors
for row in post.ho_select(json_agg={'author_fk': ['last_name']}):
print(row['author_fk']) # {'last_name': 'Martin'}
# Chained FK (A ← B → C) — aggregate the leaf relation's data
# For each post, collect the persons who commented on it.
# post ← comment → person (comment is the junction)
post = Post(title='Hello')
comment = Comment()
comment.author_fk.set() # chain: comment → person
post.comment_rfk.set(comment)
for row in post.ho_select(json_agg={'comment_rfk': ['last_name']}):
print(row['comment_rfk']) # [{'last_name': '...'}, ...]
Changed in version 0.18.7 (breaking).
ho_count(*args, distinct=False)
¶
Return the number of rows that satisfy the predicate. Executes SQL.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*args
|
column names for the inner SELECT (useful with
|
()
|
|
distinct
|
bool
|
if |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
int |
the cardinality of the extension. |
Example
ho_count usage:
New in version 0.18.0: distinct parameter.
ho_is_empty()
¶
ho_update(*args, update_all=False, **kwargs)
¶
Update every row that satisfies the predicate. Executes SQL.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*args
|
column names to return from the updated rows. Pass
|
()
|
|
update_all
|
bool
|
must be |
False
|
**kwargs
|
|
{}
|
Returns:
| Type | Description |
|---|---|
|
list[dict] | None: the updated rows if |
|
|
otherwise |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
if no constraint is set and |
ho_delete(*args, delete_all=False)
¶
Remove every row that satisfies the predicate. Executes SQL.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*args
|
column names to return from the deleted rows. Pass
|
()
|
|
delete_all
|
bool
|
must be |
False
|
Returns:
| Type | Description |
|---|---|
|
list[dict] | None: the deleted rows if |
|
|
otherwise |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
if the predicate is not set and |
Async executors¶
Async counterparts of every executor. Require an async connection opened
with await model.aconnect(). Return plain values (not generators) so the
cursor can be closed before returning to the caller.
import asyncio
from half_orm.model import Model
async def main():
blog = Model('blog')
await blog.aconnect()
Author = blog.get_relation_class('blog.author')
Post = blog.get_relation_class('blog.post')
alice = await Author(
name='Alice', email='alice@example.com'
).ho_ainsert()
posts = await Post(author_id=alice['id']).ho_aselect()
n = await Post().ho_acount()
await blog.adisconnect()
asyncio.run(main())
ho_ainsert(*args)
async
¶
Async variant of ho_insert. Executes SQL.
New in version 0.18.0.
Sync counterpart: ho_insert
ho_aselect(*args, distinct=False, order_by=None, limit=None, offset=None)
async
¶
Async variant of ho_select. Returns a list of dicts (not a generator). Executes SQL.
New in version 0.18.0.
Sync counterpart: ho_select
ho_acount(*args, distinct=False)
async
¶
Async variant of ho_count. Executes SQL.
New in version 0.18.0.
Sync counterpart: ho_count
ho_ais_empty()
async
¶
Async variant of ho_is_empty. Executes SQL.
New in version 0.18.0.
Sync counterpart: ho_is_empty
ho_aupdate(*args, update_all=False, **kwargs)
async
¶
Async variant of ho_update. Executes SQL.
New in version 0.18.0.
Sync counterpart: ho_update
ho_adelete(*args, delete_all=False)
async
¶
Async variant of ho_delete. Executes SQL.
New in version 0.18.0.
Sync counterpart: ho_delete
Introspection¶
These methods inspect or assert on the predicate without executing SQL.
ho_assert_is_singleton()
¶
Assert that this predicate identifies exactly one row, without querying the database.
A predicate is a singleton when every field of a unique identifier
(primary key or any UNIQUE NOT NULL constraint) is set with the
= comparator. The check is purely structural — no SQL is executed.
Returns:
| Type | Description |
|---|---|
|
self — for chaining before a write operation. |
Raises:
| Type | Description |
|---|---|
NotASingletonError
|
if no unique identifier is fully set. |
Example
ho_is_singleton usage:
# OK — id is the primary key
Author(id=42).ho_assert_is_singleton()
# OK — email has a UNIQUE NOT NULL constraint
Author(email='alice@example.com').ho_assert_is_singleton()
# Raises — last_name is not a unique identifier
Author(last_name='Martin').ho_assert_is_singleton()
# Typical usage: guard a single-row write
Author(id=42).ho_assert_is_singleton().ho_update(email='new@example.com')
New in version 0.18.0.
ho_is_set()
¶
Return True if one field at least is set or if self has been constrained by at least one of its foreign keys or self is the result of a combination of Relations (using set operators) where at least one operand is itself constrained.
Syntactic check, not semantic — risk of data loss
ho_is_set() inspects the predicate structure only. When both operands
of a set operator are constrained, it returns True even if the
extension is semantically equivalent to the full table:
Post() & Post() # False — both operands unconstrained
Post() | Post(title='a') # True — right operand is constrained
Post(title='a') | Post(title=('!=', 'a')) # True — but = all posts
Because ho_delete() and ho_update() allow execution without
delete_all=True / update_all=True when ho_is_set() is True,
these predicates will silently operate on the entire table.
Always verify the actual extension with ho_count() or ho_mogrify()
before destructive operations on set-operator predicates.
ho_where_display()
¶
Returns the SQL JOIN and WHERE clauses as a dict, or None if no constraint.
Returns:
| Type | Description |
|---|---|
|
dict with keys: 'joins' : list of JOIN SQL strings (one per joined relation) 'where' : WHERE expression SQL string, or None 'values' : list of string values (join values first, then where values) |
|
|
or None if the relation has no constraint set. |
New in version 0.18.0.
ho_mogrify()
¶
Print the SQL SELECT that would be executed and return self.
Activates SQL tracing for the next query on this object. The query is printed to stderr when the next executor is called. Useful for debugging predicate composition.
Returns:
| Type | Description |
|---|---|
|
self — for chaining. |
ho_dict()
¶
Returns a dictionary containing only the values of the fields that are set.
ho_description()
classmethod
¶
Returns the description (comment) of the relation
Decorators¶
singleton(fct)
¶
Decorator that enforces a singleton predicate before calling the method.
Calls :meth:~half_orm.relation.Relation.ho_assert_is_singleton on
self before executing the decorated method. Raises
:exc:~half_orm.relation_errors.NotASingletonError if the predicate
does not identify exactly one row. No database query is performed.
Use this on any method that must operate on a single, identified row.
Example
singleton decorator usage:
@register
class Author(blog.get_relation_class('blog.author')):
Fkeys = {'post_rfk': '_reverse_fkey_blog_post_author_id'}
@singleton
def publish(self, title: str, content: str):
return self.post_rfk(title=title, content=content).ho_insert()
Author(id=1).publish('My post', 'Content here') # OK
Author(last_name='Martin').publish('…', '…') # raises NotASingletonError
Changed in version 0.18.0: the check is now purely structural (no database query).
transaction(fct)
¶
Decorator that wraps a Relation method in a database transaction.
Every INSERT / UPDATE / DELETE executed inside the decorated method runs inside a single atomic unit. Commits on normal return, rolls back and re-raises on any exception.
Nested @transaction calls use PostgreSQL savepoints: a failure in an
inner method rolls back only that inner scope.
See also: Transaction — the context manager equivalent.