Skip to content

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.