Entity¶
- class nextorm.entity.Entity[source]¶
Bases:
objectBase class for all NextORM entities.
Subclass
Entityand annotate fields with NextORM markers to define a mapped table:from nextorm.fields import Req, Opt, PK, Single, Set, Local class User(Entity): name: Req[str] email: Req[str] = Req[str](unique=True) age: Opt[int] class Post(Entity): title: Req[str] body: Req[LongStr] author: Single[User] tags: Set[Tag]
Primary keys — a
PK[int]auto-increment column namedidis injected automatically when no PK is declared. Override explicitly:class Event(Entity): id: PK[uuid7] # time-ordered UUID, Python-generated title: Req[str]
Table name — defaults to the class name lower-cased. Override:
class BlogPost(Entity): _table_ = "blog_post" title: Req[str]
Composite primary keys — use
PrimaryKey():class OrderLine(Entity): order: Single[Order] product: Single[Product] quantity: Req[int] _pk_ = PrimaryKey("order", "product")
Single-table inheritance (STI) — declare
_discriminator_col_on the parent and_discriminator_on each child; all share one SQL table:class Animal(Entity): _discriminator_col_ = "kind" name: Req[str] class Dog(Animal): _discriminator_ = "dog" breed: Opt[str]
- __init__(**kwargs)[source]¶
Initialise a new (unsaved) entity instance.
Pass field values as keyword arguments. Fields not provided here receive their
defaultvalue (if one was declared) orNone:u = User(name="alice", email="alice@example.com")
If an active session exists, the new instance is automatically scheduled for INSERT on the next
flushcall.- Parameters:
kwargs (Any)
- Return type:
None
- after_load()[source]¶
Called after an existing entity row is loaded from the database.
Override to initialise
Localfields or to compute derived state from persisted values:class Product(Entity): name: Req[str] price: Req[float] _display: Local[str] def after_load(self) -> None: self._display = f"{self.name} — ${self.price:.2f}"
- Return type:
None
- before_insert()[source]¶
Called immediately before a new entity is written to the database.
Override to set computed fields or perform validation before the first
INSERT:class Article(Entity): title: Req[str] slug: Req[str] def before_insert(self) -> None: if not self.slug: self.slug = self.title.lower().replace(" ", "-")
- Return type:
None
- after_insert()[source]¶
Called after a new entity has been written to the database.
The auto-generated primary key (if any) is available at this point:
def after_insert(self) -> None: print(f"Saved with id={self.id}")
- Return type:
None
- before_update()[source]¶
Called immediately before a dirty entity is flushed to the database.
Override to update timestamps or enforce invariants before
UPDATE:class Post(Entity): updated_at: Req[datetime] def before_update(self) -> None: self.updated_at = datetime.now()
- Return type:
None
- after_update()[source]¶
Called after a modified entity has been successfully written to the database.
- Return type:
None
- before_delete()[source]¶
Called immediately before an entity is deleted from the database.
Override to perform cleanup or cascading logic that cannot be expressed in the schema:
def before_delete(self) -> None: self._cleanup_files()
- Return type:
None
- after_delete()[source]¶
Called after an entity has been successfully deleted from the database.
- Return type:
None
- get_pk()[source]¶
Return the primary key value of this entity instance.
Returns
Nonewhen the entity has no primary key field or when the PK has not been set yet (e.g. before the firstdb.save()call).Example:
u = User(name="alice") db.save(u) pk = u.get_pk() # → 1
- Return type:
- set(**kwargs)[source]¶
Bulk-assign field values in a single call.
Each assignment goes through the descriptor, so enum coercion, validation,
autostrip, and dirty-tracking all apply:user.set(name="bob", age=31) # equivalent to: user.name = "bob" user.age = 31
- Parameters:
kwargs (Any)
- Return type:
None
- delete()[source]¶
Delete this entity from the database using its attached database context.
The entity must have been loaded or saved through a
Databaseso that the_db_context attribute is set. Usedelete_instance()directly if you want to pass the database explicitly.post = db.select(Post).filter(Post.id == 42).fetch_one() post.delete()
Raises
RuntimeErrorwhen the entity has no attached database context (_db_not set).- Return type:
None
- flush()[source]¶
Persist this entity’s pending changes to the database immediately.
Saves this specific entity — an INSERT when the entity is new (PK not yet assigned), or an UPDATE for dirty fields. Only this instance is written; other entities in the session are unaffected.
The database is resolved in this order:
The
_db_context attribute set by a previousdb.save()ordb.select(...)call.The mapped
Databaselocated via_find_db_for_entity().
Raises
RuntimeErrorwhen no mapped database can be found.with db_session: order = Order(ref="ORD-001") order.flush() # writes only this order — no full session flush print(order.id) # PK available immediately after flush
- Return type:
None
- commit()[source]¶
Persist this entity’s changes and commit the underlying transaction.
Equivalent to calling
flush()followed bycommit()on the attached database. Commits the entire transaction of the attached connection, which is the same scopedb.commit()would commit — only this entity’s changes are guaranteed to have been flushed beforehand.The database is resolved the same way as
flush().with db_session: order = Order(ref="ORD-001") order.commit() # flush + commit — order is durable immediately
- Return type:
None
- classmethod get(**kwargs)[source]¶
Return the first entity matching all given field values, or
None.Locates the mapped database automatically via
_find_db_for_entity(). RaisesRuntimeErrorif more than one row matches.user = User.get(email="alice@example.com") if user is None: print("not found")
For more complex filtering use
select():user = User.select().filter(User.age > 18).get()
- classmethod exists(**kwargs)[source]¶
Return
Trueif at least one entity matches all given field values.if User.exists(email="alice@example.com"): raise ValueError("Email already in use")
- classmethod select()[source]¶
Return a
QuerySetfor this entity.Locates the mapped database automatically. Chain filter, ordering, and fetch methods on the returned queryset:
active = User.select().filter(User.active == True).fetch_all() count = User.select().count() first = User.select().filter(User.age > 18).get()
- Return type:
- classmethod aselect()[source]¶
Return an
AsyncQuerySetfor this entity.Returns the queryset synchronously — no
awaitneeded on this call.awaitis required when executing the query:users = await User.aselect().filter(User.active == True).fetch_all() count = await User.aselect().count()
Raises
RuntimeErrorif the entity is mapped to a syncDatabaserather than anAsyncDatabase.- Return type:
- async classmethod aget(**kwargs)[source]¶
Async equivalent of
get().Return the first entity matching all given field values, or
None. RaisesRuntimeErrorif more than one row matches.user = await User.aget(email="alice@example.com")
- to_dict(only=None, exclude=None, *, with_collections=False, with_lazy=False, related_objects=False)[source]¶
Serialize this entity to a plain Python dictionary.
Parameters¶
- only:
If given, only the listed field names are included. Relations are included only when with_collections or related_objects is also
True.- exclude:
Field names to exclude. Applied after only.
- with_collections:
When
True,Set[T]relation attributes are included as lists of theirto_dict()results. Requires the collections to have been prefetched or lazily loaded beforehand.- with_lazy:
When
False(the default) lazy fields that have not yet been loaded are omitted from the result. PassTrueto include them; unloaded lazy fields are fetched on demand (sync databases only).- related_objects:
When
True, loadedSingle[T]relation attributes are included as nestedto_dict()results. If the related object has not been loaded, the raw FK value ({name}_id) is included instead.
Example:
user.to_dict() # → {"id": 1, "name": "alice", "age": 30} user.to_dict(exclude=["id"]) # → {"name": "alice", "age": 30} user.to_dict(with_collections=True) # → {"id": 1, ..., "posts": [{"id": 5, "title": "hi", ...}]} article.to_dict(with_lazy=True) # → {"id": 1, "title": "hi", "body": "<full text>"} comment.to_dict(related_objects=True) # → {"id": 1, "text": "great", "author": {"id": 2, "name": "bob"}}
- classmethod select_by_sql(db, sql, params=None)[source]¶
Execute sql and return all rows mapped to instances of this entity.
Convenience wrapper around
db.select(cls).raw(sql, params).Example:
users = User.select_by_sql(db, "SELECT * FROM user WHERE age > ?", [18])
- classmethod get_by_sql(db, sql, params=None)[source]¶
Execute sql and return the first row as an entity instance, or
None.Convenience wrapper around
db.select(cls).raw_one(sql, params).Example:
user = User.get_by_sql(db, "SELECT * FROM user WHERE id = ?", [1])
- async classmethod aselect_by_sql(db, sql, params=None)[source]¶
Async equivalent of
select_by_sql().Example:
users = await User.aselect_by_sql(db, "SELECT * FROM user WHERE age > %s", [18])
- Parameters:
db (AsyncDatabase)
sql (str)
params (list[Any] | None)
- Return type:
list[Self]
- async classmethod aget_by_sql(db, sql, params=None)[source]¶
Async equivalent of
get_by_sql().Example:
user = await User.aget_by_sql(db, "SELECT * FROM user WHERE id = %s", [1])
- Parameters:
db (AsyncDatabase)
sql (str)
params (list[Any] | None)
- Return type:
Self | None
- class nextorm.entity.EntityMeta[source]¶
Bases:
typeMetaclass that processes field annotations at class-definition time.
When a class that inherits from
Entityis created,EntityMetascans its annotations for NextORM markers (Req,Opt,PK,Single,Set,Local) and performs the following steps:Collects all annotations from the full MRO so that inherited fields are included.
For each field marker annotation it builds a
FieldSpec, merging type-specific options from the marker call site, and installs aFieldDescriptor.For each relation marker it builds a
RelationSpecand installs aSingleDescriptororSetDescriptor.If no primary key is declared, injects an auto-increment
id: PK[int]column automatically.Registers the class in the global
_entity_registryso thatDatabasecan discover it duringgenerate_mapping().
The following class-level attributes are set on every
Entitysubclass:_fields_dict[str, FieldInfo]All persistent scalar fields keyed by attribute name.
_relations_dict[str, RelationInfo]All relation fields keyed by attribute name.
_locals_dict[str, LocalInfo]All transient
Localfields keyed by attribute name, each carrying its resolvedLocalSpec(default value, py_check)._pk_fields_tuple[str, ...]Attribute names of all primary-key fields (one for scalar PKs, multiple for composite PKs declared with
PrimaryKey())._pk_field_str | NoneThe single PK attribute name for entities with a scalar PK;
Nonefor composite PKs._table_name_strSQL table name — defaults to the class name lower-cased; override by declaring
_table_ = "my_table"in the class body._constraints_list[CompositeConstraint]Non-PK composite unique constraints and indexes declared with
composite_key()orcomposite_index().
Single-table inheritance (STI) is supported by declaring
_discriminator_col_on the parent entity and_discriminator_on each child:class Animal(Entity): _discriminator_col_ = "kind" name: Req[str] class Dog(Animal): _discriminator_ = "dog" breed: Opt[str]
- class nextorm.entity.FieldInfo[source]¶
Bases:
objectResolved metadata for a single persistent field, stored in
cls._fields_.Read-only after creation by
EntityMeta. Available for introspection at runtime:for name, fi in Product._fields_.items(): print(name, fi.py_type, fi.spec.nullable)
Attributes¶
- name:
The attribute name as declared in the entity class.
- py_type:
The Python type used to store and validate the value (e.g.
int,str,Vec). UUID sentinel types (uuid7,uuid4,ulid) are normalised touuid.UUIDorULIDhere.- spec:
The
FieldSpeccontaining all column options.
- name¶
- py_type¶
- spec¶
- class nextorm.entity.RelationInfo[source]¶
Bases:
objectResolved metadata for a relation field, stored in
cls._relations_.Read-only after creation by
EntityMeta. Available for introspection at runtime:for name, ri in Comment._relations_.items(): print(name, ri.spec.kind, ri.spec.target)
Attributes¶
- name:
The attribute name as declared in the entity class.
- spec:
The
RelationSpeccontaining all relation options (kind, target entity, nullable, FK column name, etc.).
- __init__(name, spec)[source]¶
- Parameters:
name (str)
spec (RelationSpec)
- Return type:
None
- name¶
- spec¶
- class nextorm.collection.RelatedCollection[source]¶
Bases:
GenericA lazy, database-backed collection representing one side of a relation.
Instances are created by
SetDescriptoron first attribute access. They hold a weak logical reference to the owner entity so that individual collections can be GC’d when not needed.The collection is not thread-safe. Use separate collection objects in separate threads.
Parameters¶
- owner:
The entity instance that owns this relation attribute.
- ri:
The
RelationInfofor this relation.- db:
The
Databaseto query.Nonewhen the owner entity was not loaded from a database (e.g. a freshly created but unsaved entity).
- __init__(owner, ri, db)[source]¶
- Parameters:
owner (Entity)
ri (RelationInfo)
db (Database | None)
- Return type:
None
- filter(*conditions)[source]¶
Return a filtered
QuerySetfor this collection.- Parameters:
conditions (Any)
- Return type:
QuerySet[T]
- order_by(*items)[source]¶
Return an ordered
QuerySetfor this collection.- Parameters:
items (Any)
- Return type:
QuerySet[T]
- page(pagenum, pagesize=10)[source]¶
Return a page of this collection’s items (1-based page numbers).
- add(*items)[source]¶
Add one or more items to this collection.
For M2M: inserts rows into the join table. For O2M: updates the FK column on each item.
- Parameters:
items (T)
- Return type:
None