CRUD and transactions¶
Every generated entity and relation class exposes a concise manager bound to its verified package projection.
from app_models import Age, Person, PersonId
ada = Person(person_id=PersonId("ada"), age=Age(36))
Person.manager(db).put(ada)
people = Person.manager(db).filter(age__gte=18).all()
No model registry or handwritten descriptor is involved. Native execution accepts only the exact generated class and value wrappers installed by that package.
Entity manager operations¶
| Operation | Result |
|---|---|
insert(value) |
Insert one model and attach its IID. |
insert_many(values) |
Insert one homogeneous batch atomically. |
put(value) |
Idempotently match/insert by the declared key. |
put_many(values) |
Put one homogeneous batch atomically. |
update(value) |
Update by attached IID, or resolve a detached Python model by its projected key. |
delete(value_or_iid) |
Delete by IID, or resolve a detached Python model by its projected key. |
get_by_iid(iid) |
Return one hydrated model or None. |
filter(**lookups) |
Return a new immutable filtered manager. |
where(field, comparison, value) |
Start an immutable exact field-token filter. |
all() / first() |
Materialize all or the first match. |
count() / exists() |
Execute database-side terminals. |
manager = Person.manager(db)
ada = manager.insert(Person(person_id=PersonId("ada"), age=Age(36)))
assert ada.iid is not None
ada.age = Age(37)
manager.update(ada)
assert manager.get_by_iid(ada.iid).age.value == 37
manager.delete(ada)
assert manager.get_by_iid(ada.iid) is None
For Python, update and model-valued delete use an attached canonical TypeDB
IID when present. A detached model is resolved by its projected key; if no row
has that key, the operation is a no-op and the model remains detached. Passing
an IID string to delete keeps exact-IID behavior. put requires the generated
model's projected key contract.
Filters¶
Generated managers retain the compatibility filter(...) surface for IID,
raw-scalar, rich-operator, and filtered-mutation workflows. Its scalar lookup
suffixes are eq, ne, gt, gte, lt, and lte; no suffix means
equality.
adults = Person.manager(db).filter(age__gte=18)
assert adults.exists()
first = adults.first()
count = adults.count()
Pass the exact generated attribute wrapper or a compatible target-language scalar. An exact wrapper is useful when the scalar domain could be ambiguous.
Generated field names may contain __. A complete field-name match wins unless
the key ends with a supported lookup whose prefix is also a field. Use an
explicit trailing __eq to select a field that collides with a lookup spelling:
manager.filter(**{"foo__bar": FooBar(7)})
manager.filter(**{"score__gte__eq": ScoreGte(8)})
manager.filter(score__gte=Score(18))
The equivalent generated TypeScript filters use an object and generated target names:
Person.manager(db).filter({ score__gte: Score.create(18n) }).all();
Person.manager(db).filter({ scoreGte__eq: ScoreGte.create(8n) }).all();
For an exact generated-field contract, use the distinct immutable where
filter. It accepts an issued field token, one of the closed six comparisons,
and the field's exact generated attribute value. In TypeScript this surface is
emitted by ordered successor packages; legacy unordered packages remain
byte-exact.
from app_models import ProjectedManagerComparison
root = Person.manager(db).where()
adult = root.where(Person.age, ProjectedManagerComparison.GTE, Age(18))
assert root.count() >= adult.count() # the parent remains reusable
ada = Person.manager(db).where(
Person.person_id,
ProjectedManagerComparison.EQ,
PersonId("ada"),
).first()
const root = Person.manager(db).where();
const adult = root.where(Person.fields.age, "gte", Age.create(18n));
const ada = Person.manager(db)
.where(Person.fields.personId, "eq", PersonId.create("ada"))
.first();
Canonical filters expose only where, all, first, count, and exists.
They cannot mutate or accept string/IID/rich-operator escapes. Their first
is identity-strict: every effective reference-key field needs one semantically
consistent equality predicate. An unkeyed, partial, conflicting, or otherwise
nonsingular filter fails before provider I/O. Compatibility-manager first()
retains its released arbitrary-first behavior.
Relation managers¶
Relations expose the same lifecycle and terminal operations. Their generated constructor carries the role-player values:
employment = Employment(employee=ada, employer=acme, since=Since(today))
Employment.manager(db).insert(employment)
stored = Employment.manager(db).get_by_iid(employment.iid)
Employment.manager(db).delete(stored)
Role players are lowered and hydrated through the installed projection. Exact
classes, reference forms, keys, and allowed plays facts are revalidated before
execution.
Batches¶
people = [
Person(person_id=PersonId("ada"), age=Age(36)),
Person(person_id=PersonId("grace"), age=Age(45)),
]
Person.manager(db).insert_many(people)
Person.manager(db).put_many(people)
A bulk call uses one transaction and returns values in input order. On supported
TypeDB 3.12/band-9 connections, eligible homogeneous entity inserts may use the
compiled given-stage fast path; fallback execution has the same result.
Caller-owned transactions¶
Pass a transaction instead of a database to reuse it across generated managers and query sessions:
with db.transaction("write") as transaction:
person_manager = Person.manager(transaction)
employment_manager = Employment.manager(transaction)
person = person_manager.put(
Person(person_id=PersonId("ada"), age=Age(36))
)
employment_manager.insert(Employment(employee=person, employer=acme))
The context commits on normal exit and rolls back when an exception escapes.
A generated manager never commits a caller-owned transaction. Read transactions
can be shared with Person.query(transaction) for multiple terminal calls.
Canonical manager filters can borrow that same read transaction; each terminal
leaves it active for sibling filters and typed queries.
Language differences¶
- Generated Python values are mutable;
update(value)replaces the attached IID. Canonical filters accept only field tokens issued by that generated Python package instance. - Generated TypeScript values are immutable; use
update(iid, replacement). Canonical filters require the package-local token identity retained by the generated runtime. - Generated Rust managers are async and use generated create/model/reference types; write transactions expose transaction-bound managers. Rust rejects foreign generated token types statically and revalidates token metadata at runtime.
These are language-boundary differences. Entity/relation CRUD, IID behavior, batch atomicity, filtering, terminals, and commit/rollback outcomes are covered by the cross-binding generated-operation parity gate.
Use a package-local immutable query for joins, traversal, selected shapes, ordering, pages, reductions, or grouping.