Skip to content

type_bridge.models

models

TypeDB model classes - Entity, Relation, and Role.

TypeDBType

Bases: BaseModel, ABC

Abstract base class for TypeDB entities and relations.

This class provides common functionality for both Entity and Relation types, including type name management, abstract/base flags, and attribute ownership.

Subclasses must implement: - get_supertype(): Get parent type in TypeDB hierarchy - to_schema_definition(): Generate TypeQL schema definition - to_insert_query(): Generate TypeQL insert query for instances

manager classmethod

manager(connection)

Create a CRUD manager for this type.

Parameters:

Name Type Description Default
connection Connection

Database, Transaction, or TransactionContext

required

Returns:

Type Description
TypeDBManager[Self]

Manager instance for this type

Source code in type_bridge/models/base.py
@classmethod
def manager(cls, connection: Connection) -> TypeDBManager[Self]:
    """Create a CRUD manager for this type.

    Args:
        connection: Database, Transaction, or TransactionContext

    Returns:
        Manager instance for this type
    """
    manager_class = cls._get_manager_class()
    return cast("TypeDBManager[Self]", manager_class(connection, cls))

insert

insert(connection)

Insert this instance into the database.

Parameters:

Name Type Description Default
connection Connection

Database, Transaction, or TransactionContext

required

Returns:

Type Description
Self

Self for chaining

Source code in type_bridge/models/base.py
def insert(self, connection: Connection) -> Self:
    """Insert this instance into the database.

    Args:
        connection: Database, Transaction, or TransactionContext

    Returns:
        Self for chaining
    """
    self.manager(connection).insert(self)
    return self

delete

delete(connection)

Delete this instance from the database.

Parameters:

Name Type Description Default
connection Connection

Database, Transaction, or TransactionContext

required

Returns:

Type Description
Self

Self for chaining

Source code in type_bridge/models/base.py
def delete(self, connection: Connection) -> Self:
    """Delete this instance from the database.

    Args:
        connection: Database, Transaction, or TransactionContext

    Returns:
        Self for chaining
    """
    self.manager(connection).delete(self)
    return self

has classmethod

has(connection, attr_class, value=None)

Find all instances of this class (and its subtypes) that own attr_class.

Behaviour depends on the receiver:

  • Entity.has(...) / Relation.has(...): cross-type lookup — returns instances across all concrete types of that kind.
  • <ConcreteType>.has(...) / <AbstractBase>.has(...): narrowed lookup — restricted to that type and its TypeDB subtypes via isa polymorphism.

Returned relation instances always have their role players hydrated (in addition to attributes). This is implemented by re-fetching each relation through concrete_class.manager(connection).get(_iid=...) after the initial wildcard query, so the relation path is N+1 in the number of returned relations. Entity lookups remain single-query.

Parameters:

Name Type Description Default
connection Connection

Database, Transaction, or TransactionContext.

required
attr_class type[Attribute]

Attribute type to search for (e.g. Name).

required
value Any | None

Optional filter — raw value, Attribute instance, or Expression (e.g. Name.gt(Name("B"))).

None

Returns:

Type Description
list[TypeDBType]

List of hydrated model instances (may contain mixed concrete types

list[TypeDBType]

when called on the base Entity / Relation class or an

list[TypeDBType]

abstract base subclass).

Raises:

Type Description
TypeError

If called directly on :class:TypeDBType (use :class:Entity or :class:Relation instead).

Source code in type_bridge/models/base.py
@classmethod
def has(
    cls,
    connection: Connection,
    attr_class: type[Attribute],
    value: Any | None = None,
) -> list[TypeDBType]:
    """Find all instances of this class (and its subtypes) that own *attr_class*.

    Behaviour depends on the receiver:

    * ``Entity.has(...)`` / ``Relation.has(...)``: cross-type lookup — returns
      instances across **all** concrete types of that kind.
    * ``<ConcreteType>.has(...)`` / ``<AbstractBase>.has(...)``: narrowed
      lookup — restricted to that type and its TypeDB subtypes via ``isa``
      polymorphism.

    Returned relation instances always have their role players hydrated
    (in addition to attributes). This is implemented by re-fetching each
    relation through ``concrete_class.manager(connection).get(_iid=...)``
    after the initial wildcard query, so the relation path is N+1 in the
    number of returned relations. Entity lookups remain single-query.

    Args:
        connection: Database, Transaction, or TransactionContext.
        attr_class: Attribute type to search for (e.g. ``Name``).
        value: Optional filter — raw value, Attribute instance,
               or Expression (e.g. ``Name.gt(Name("B"))``).

    Returns:
        List of hydrated model instances (may contain mixed concrete types
        when called on the base ``Entity`` / ``Relation`` class or an
        abstract base subclass).

    Raises:
        TypeError: If called directly on :class:`TypeDBType` (use
            :class:`Entity` or :class:`Relation` instead).
    """
    from type_bridge.crud.has_lookup import has_lookup
    from type_bridge.models.entity import Entity
    from type_bridge.models.relation import Relation

    if cls is TypeDBType:
        raise TypeError("has() must be called on Entity or Relation, not TypeDBType directly")

    if issubclass(cls, Entity):
        kind: Literal["entity", "relation"] = "entity"
        base_cls: type[TypeDBType] = Entity
    elif issubclass(cls, Relation):
        kind = "relation"
        base_cls = Relation
    else:
        raise TypeError(f"has() requires an Entity or Relation class, got {cls.__name__}")

    # Narrow to the concrete (or abstract base) type when the caller is
    # not the bare Entity / Relation class. TypeDB's `isa` is polymorphic,
    # so subtypes of an abstract base are matched automatically.
    narrow_type = None if cls is base_cls else cls.get_type_name()

    return has_lookup(connection, attr_class, value, kind=kind, type_name=narrow_type)

__init_subclass__

__init_subclass__()

Called when a TypeDBType subclass is created.

Source code in type_bridge/models/base.py
def __init_subclass__(cls) -> None:
    """Called when a TypeDBType subclass is created."""
    super().__init_subclass__()

    # Get TypeFlags if defined, otherwise create new default flags
    # Check if flags is defined directly on this class (not inherited)
    if "flags" in cls.__dict__ and isinstance(cls.__dict__["flags"], TypeFlags):
        # Explicitly set flags on this class
        cls._flags = cls.__dict__["flags"]
    else:
        # No explicit flags on this class - create new default flags
        # This ensures each subclass gets its own flags instance
        cls._flags = TypeFlags()

    # Validate type name doesn't conflict with TypeDB built-ins
    # Skip validation for:
    # 1. Base classes that won't appear in schema (base=True)
    # 2. The abstract base Entity and Relation classes themselves
    is_base_entity_or_relation = cls.__name__ in ("Entity", "Relation") and cls.__module__ in (
        "type_bridge.models",
        "type_bridge.models.entity",
        "type_bridge.models.relation",
    )
    if not cls._flags.base and not is_base_entity_or_relation:
        type_name = cls._flags.name or format_type_name(cls.__name__, cls._flags.case)
        validate_type_name(type_name, cls.__name__, cls._type_context)

    # Register model in the central registry
    ModelRegistry.register(cls)

__pydantic_init_subclass__ classmethod

__pydantic_init_subclass__(**kwargs)

Called by Pydantic after model class initialization.

Injects FieldDescriptor instances for class-level query access. This runs after Pydantic's setup is complete, so descriptors won't be removed.

Example

Person.age # Returns FieldRef for query building (class-level access) person.age # Returns attribute value (instance-level access)

Source code in type_bridge/models/base.py
@classmethod
def __pydantic_init_subclass__(cls, **kwargs: Any) -> None:
    """Called by Pydantic after model class initialization.

    Injects FieldDescriptor instances for class-level query access.
    This runs after Pydantic's setup is complete, so descriptors won't be removed.

    Example:
        Person.age  # Returns FieldRef for query building (class-level access)
        person.age  # Returns attribute value (instance-level access)
    """
    super().__pydantic_init_subclass__(**kwargs)

    from type_bridge.fields import FieldDescriptor

    # Inject FieldDescriptors for class-level query access
    for field_name, attr_info in cls._owned_attrs.items():
        descriptor = FieldDescriptor(field_name=field_name, attr_type=attr_info.typ)
        type.__setattr__(cls, field_name, descriptor)

model_copy

model_copy(*, update=None, deep=False)

Override model_copy to ensure raw values are wrapped in Attribute instances.

Pydantic's model_copy bypasses validators even with revalidate_instances='always', so we pre-wrap values in the update dict before copying. Also preserves _iid from original using Pydantic's pydantic_private.

Source code in type_bridge/models/base.py
def model_copy(self, *, update: Mapping[str, Any] | None = None, deep: bool = False):
    """Override model_copy to ensure raw values are wrapped in Attribute instances.

    Pydantic's model_copy bypasses validators even with revalidate_instances='always',
    so we pre-wrap values in the update dict before copying.
    Also preserves _iid from original using Pydantic's __pydantic_private__.
    """
    # Preserve _iid before copy
    preserved_iid = getattr(self, "_iid", None)

    # Pre-wrap values in update dict before calling super()
    wrapped_update: dict[str, Any] | None = None
    if update:
        wrapped_update = {}
        owned_attrs = self.__class__.get_owned_attributes()
        for key, value in update.items():
            if key in owned_attrs and value is not None:
                attr_info = owned_attrs[key]
                attr_class = attr_info.typ
                if isinstance(value, list):
                    wrapped_update[key] = [
                        item if isinstance(item, attr_class) else attr_class(item)
                        for item in value
                    ]
                elif not isinstance(value, attr_class):
                    wrapped_update[key] = attr_class(value)
                else:
                    wrapped_update[key] = value
            else:
                wrapped_update[key] = value

    # Call parent model_copy with pre-wrapped update
    copied = super().model_copy(update=wrapped_update, deep=deep)

    # Restore _iid using Pydantic's official private attribute storage
    private = copied.__pydantic_private__
    if preserved_iid is not None and private is not None and private.get("_iid") is None:
        private["_iid"] = preserved_iid

    return copied

get_type_name classmethod

get_type_name()

Get the TypeDB type name for this type.

If name is explicitly set in TypeFlags, it is used as-is. Otherwise, the class name is formatted according to the case parameter.

Source code in type_bridge/models/base.py
@classmethod
def get_type_name(cls) -> str:
    """Get the TypeDB type name for this type.

    If name is explicitly set in TypeFlags, it is used as-is.
    Otherwise, the class name is formatted according to the case parameter.
    """
    if cls._flags.name:
        return cls._flags.name
    return format_type_name(cls.__name__, cls._flags.case)

get_supertype classmethod

get_supertype()

Get the supertype from Python inheritance, skipping base classes.

Base classes (with base=True) are Python-only and don't appear in TypeDB schema. This method skips them when determining the TypeDB supertype.

Returns:

Type Description
str | None

Type name of the parent class, or None if direct subclass

Source code in type_bridge/models/base.py
@classmethod
def get_supertype(cls) -> str | None:
    """Get the supertype from Python inheritance, skipping base classes.

    Base classes (with base=True) are Python-only and don't appear in TypeDB schema.
    This method skips them when determining the TypeDB supertype.

    Returns:
        Type name of the parent class, or None if direct subclass
    """
    base_class = cls._get_base_type_class()
    for base in cls.__bases__:
        if base is not base_class and issubclass(base, base_class):
            # Skip base classes - they don't appear in TypeDB schema
            if base.is_base():
                # Recursively find the first non-base parent
                return base.get_supertype()
            return base.get_type_name()
    return None

is_abstract classmethod

is_abstract()

Check if this is an abstract type.

Source code in type_bridge/models/base.py
@classmethod
def is_abstract(cls) -> bool:
    """Check if this is an abstract type."""
    return cls._flags.abstract

is_base classmethod

is_base()

Check if this is a Python base class (not in TypeDB schema).

Source code in type_bridge/models/base.py
@classmethod
def is_base(cls) -> bool:
    """Check if this is a Python base class (not in TypeDB schema)."""
    return cls._flags.base

get_owned_attributes classmethod

get_owned_attributes()

Get attributes owned directly by this type (not inherited).

Returns:

Type Description
dict[str, ModelAttrInfo]

Dictionary mapping field names to ModelAttrInfo (typ + flags)

Source code in type_bridge/models/base.py
@classmethod
def get_owned_attributes(cls) -> dict[str, ModelAttrInfo]:
    """Get attributes owned directly by this type (not inherited).

    Returns:
        Dictionary mapping field names to ModelAttrInfo (typ + flags)
    """
    return cls._owned_attrs.copy()

get_all_attributes classmethod

get_all_attributes()

Get all attributes including inherited ones.

Traverses the class hierarchy to collect all owned attributes, including those from parent Entity/Relation classes.

Returns:

Type Description
dict[str, ModelAttrInfo]

Dictionary mapping field names to ModelAttrInfo (typ + flags)

Source code in type_bridge/models/base.py
@classmethod
def get_all_attributes(cls) -> dict[str, ModelAttrInfo]:
    """Get all attributes including inherited ones.

    Traverses the class hierarchy to collect all owned attributes,
    including those from parent Entity/Relation classes.

    Returns:
        Dictionary mapping field names to ModelAttrInfo (typ + flags)
    """
    all_attrs: dict[str, ModelAttrInfo] = {}

    # Traverse MRO in reverse to get parent attributes first
    # Child attributes will override parent attributes with same name
    for base in reversed(cls.__mro__):
        if hasattr(base, "_owned_attrs") and isinstance(base._owned_attrs, dict):
            all_attrs.update(dict(base._owned_attrs))

    return all_attrs

get_polymorphic_attributes classmethod

get_polymorphic_attributes()

Get all attributes including those from registered subtypes.

For polymorphic queries where the base class is used but concrete subtypes may be returned, this method collects attributes from all known subtypes so the query can fetch all possible attributes.

Returns:

Type Description
dict[str, ModelAttrInfo]

Dictionary mapping field names to ModelAttrInfo, including

dict[str, ModelAttrInfo]

attributes from all registered subtypes.

Source code in type_bridge/models/base.py
@classmethod
def get_polymorphic_attributes(cls) -> dict[str, ModelAttrInfo]:
    """Get all attributes including those from registered subtypes.

    For polymorphic queries where the base class is used but concrete
    subtypes may be returned, this method collects attributes from all
    known subtypes so the query can fetch all possible attributes.

    Returns:
        Dictionary mapping field names to ModelAttrInfo, including
        attributes from all registered subtypes.
    """
    # Start with this class's attributes (including inherited)
    all_attrs = cls.get_all_attributes()

    # Recursively collect attributes from all subclasses
    def collect_subclass_attrs(klass: type[TypeDBType]) -> None:
        for subclass in klass.__subclasses__():
            # Skip if subclass is a base class (abstract, Python-only)
            if hasattr(subclass, "is_base") and subclass.is_base():
                continue

            # Get subclass attributes and merge (subclass attrs take precedence)
            if hasattr(subclass, "get_all_attributes"):
                subclass_attrs = subclass.get_all_attributes()
                for field_name, attr_info in subclass_attrs.items():
                    if field_name not in all_attrs:
                        all_attrs[field_name] = attr_info

            # Recurse into further subclasses
            collect_subclass_attrs(subclass)

    collect_subclass_attrs(cls)
    return all_attrs

to_schema_definition abstractmethod classmethod

to_schema_definition()

Generate TypeQL schema definition for this type.

Returns:

Type Description
str | None

TypeQL schema definition string, or None if this is a base class

Source code in type_bridge/models/base.py
@classmethod
@abstractmethod
def to_schema_definition(cls) -> str | None:
    """Generate TypeQL schema definition for this type.

    Returns:
        TypeQL schema definition string, or None if this is a base class
    """
    ...

get_match_clause_info abstractmethod

get_match_clause_info(var_name='$x')

Get information to build a TypeQL match clause for this instance.

Used by TypeDBManager for delete/update operations. Returns IID-based matching when available, otherwise falls back to type-specific identification (key attributes for entities, role players for relations).

Parameters:

Name Type Description Default
var_name str

Variable name to use in the match clause

'$x'

Returns:

Type Description
MatchClauseInfo

MatchClauseInfo with main clause, extra clauses, and variable name

Raises:

Type Description
ValueError

If instance cannot be identified (no IID and no keys/role players)

Source code in type_bridge/models/base.py
@abstractmethod
def get_match_clause_info(self, var_name: str = "$x") -> MatchClauseInfo:
    """Get information to build a TypeQL match clause for this instance.

    Used by TypeDBManager for delete/update operations. Returns IID-based
    matching when available, otherwise falls back to type-specific
    identification (key attributes for entities, role players for relations).

    Args:
        var_name: Variable name to use in the match clause

    Returns:
        MatchClauseInfo with main clause, extra clauses, and variable name

    Raises:
        ValueError: If instance cannot be identified (no IID and no keys/role players)
    """
    ...

to_ast abstractmethod

to_ast(var='$x')

Generate AST InsertClause for this instance.

Parameters:

Name Type Description Default
var str

Variable name to use

'$x'

Returns:

Type Description
Any

InsertClause containing statements

Source code in type_bridge/models/base.py
@abstractmethod
def to_ast(self, var: str = "$x") -> Any:
    """Generate AST InsertClause for this instance.

    Args:
        var: Variable name to use

    Returns:
        InsertClause containing statements
    """
    ...

to_insert_query

to_insert_query(var='$e')

Generate TypeQL insert query string for this instance.

This is a convenience method that uses the AST-based generation internally and compiles it to a string.

Parameters:

Name Type Description Default
var str

Variable name to use (default: "$e")

'$e'

Returns:

Type Description
str

TypeQL insert query string

Source code in type_bridge/models/base.py
def to_insert_query(self, var: str = "$e") -> str:
    """Generate TypeQL insert query string for this instance.

    This is a convenience method that uses the AST-based generation
    internally and compiles it to a string.

    Args:
        var: Variable name to use (default: "$e")

    Returns:
        TypeQL insert query string
    """
    from type_bridge.query.compiler import QueryCompiler

    insert_clause = self.to_ast(var=var)
    return QueryCompiler().compile(insert_clause)

Entity

Bases: TypeDBType

Base class for TypeDB entities with Pydantic validation.

Entities own attributes defined as Attribute subclasses. Use TypeFlags to configure type name and abstract status. Supertype is determined automatically from Python inheritance.

This class inherits from TypeDBType and Pydantic's BaseModel, providing: - Automatic validation of attribute values - JSON serialization/deserialization - Type checking and coercion - Field metadata via Pydantic's Field()

Example

class Name(String): pass

class Age(Integer): pass

class Person(Entity): flags = TypeFlags(name="person") name: Name = Flag(Key) age: Age

Abstract entity

class AbstractPerson(Entity): flags = TypeFlags(abstract=True) name: Name

Inheritance (Person sub abstract-person)

class ConcretePerson(AbstractPerson): age: Age

__init_subclass__

__init_subclass__()

Called when Entity subclass is created.

Source code in type_bridge/models/entity.py
def __init_subclass__(cls) -> None:
    """Called when Entity subclass is created."""
    super().__init_subclass__()
    logger.debug(f"Initializing Entity subclass: {cls.__name__}")

    from type_bridge.models.schema_scanner import SchemaScanner

    scanner = SchemaScanner(cls)
    cls._owned_attrs = scanner.scan_attributes(is_relation=False)

    from type_bridge.models.registry import ModelRegistry

    ModelRegistry.register_attribute_owners(cls)

to_schema_definition classmethod

to_schema_definition()

Generate TypeQL schema definition for this entity.

Returns:

Type Description
str | None

TypeQL schema definition string, or None if this is a base class

Source code in type_bridge/models/entity.py
@classmethod
def to_schema_definition(cls) -> str | None:
    """Generate TypeQL schema definition for this entity.

    Returns:
        TypeQL schema definition string, or None if this is a base class
    """
    from type_bridge.typeql.annotations import format_type_annotations

    # Base classes don't appear in TypeDB schema
    if cls.is_base():
        return None

    type_name = cls.get_type_name()
    lines = []

    # Define entity type with supertype from Python inheritance
    # TypeDB 3.x syntax: entity name @abstract, sub parent,
    supertype = cls.get_supertype()
    type_annotations = format_type_annotations(abstract=cls.is_abstract())

    entity_def = f"entity {type_name}"
    if type_annotations:
        entity_def += " " + " ".join(type_annotations)
    if supertype:
        entity_def += f", sub {supertype}"

    lines.append(entity_def)

    # Add attribute ownerships using shared helper
    lines.extend(cls._build_owns_lines())

    # Join with commas, but end with semicolon (no comma before semicolon)
    return ",\n".join(lines) + ";"

to_ast

to_ast(var='$e')

Generate AST InsertClause for this instance.

Parameters:

Name Type Description Default
var str

Variable name to use

'$e'

Returns:

Type Description
InsertClause

InsertClause containing statements

Source code in type_bridge/models/entity.py
def to_ast(self, var: str = "$e") -> InsertClause:
    """Generate AST InsertClause for this instance.

    Args:
        var: Variable name to use

    Returns:
        InsertClause containing statements
    """
    from type_bridge.query.ast import InsertClause, IsaStatement, Statement

    type_name = self.get_type_name()
    statements: list[Statement] = [IsaStatement(variable=var, type_name=type_name)]

    # Add attribute statements using shared helper from TypeDBType
    statements.extend(self._build_attribute_statements(var))

    return InsertClause(statements=statements)

get_match_clause_info

get_match_clause_info(var_name='$e')

Get match clause info for this entity instance.

Prefers IID-based matching when available (most precise). Falls back to @key attribute matching.

Parameters:

Name Type Description Default
var_name str

Variable name to use in the match clause

'$e'

Returns:

Type Description
MatchClauseInfo

MatchClauseInfo with the match clause

Raises:

Type Description
ValueError

If entity has neither _iid nor key attributes

Source code in type_bridge/models/entity.py
def get_match_clause_info(self, var_name: str = "$e") -> MatchClauseInfo:
    """Get match clause info for this entity instance.

    Prefers IID-based matching when available (most precise).
    Falls back to @key attribute matching.

    Args:
        var_name: Variable name to use in the match clause

    Returns:
        MatchClauseInfo with the match clause

    Raises:
        ValueError: If entity has neither _iid nor key attributes
    """
    type_name = self.get_type_name()

    # Prefer IID-based matching when available
    entity_iid = getattr(self, "_iid", None)
    if entity_iid:
        main_clause = f"{var_name} isa {type_name}, iid {entity_iid}"
        return MatchClauseInfo(main_clause=main_clause, extra_clauses=[], var_name=var_name)

    # Fall back to key attribute matching
    key_attrs = {
        field_name: attr_info
        for field_name, attr_info in self.get_all_attributes().items()
        if attr_info.flags.is_key
    }

    if key_attrs:
        parts = [f"{var_name} isa {type_name}"]
        for field_name, attr_info in key_attrs.items():
            value = getattr(self, field_name, None)
            if value is None:
                from type_bridge.crud.exceptions import KeyAttributeError

                raise KeyAttributeError(
                    entity_type=self.__class__.__name__,
                    operation="identify",
                    field_name=field_name,
                )
            attr_name = attr_info.typ.get_attribute_name()
            parts.append(f"has {attr_name} {self._format_value(value)}")
        main_clause = ", ".join(parts)
        return MatchClauseInfo(main_clause=main_clause, extra_clauses=[], var_name=var_name)

    # Neither IID nor key attributes available
    raise ValueError(
        f"Entity '{self.__class__.__name__}' cannot be identified: "
        f"no _iid set and no @key attributes defined. Either fetch the entity from the "
        f"database first (to populate _iid) or add Flag(Key) to an attribute."
    )

get_match_pattern

get_match_pattern(var_name='$e')

Get an AST EntityPattern for matching this entity instance.

Prefers IID-based matching when available (most precise). Falls back to @key attribute matching.

Parameters:

Name Type Description Default
var_name str

Variable name to use in the pattern

'$e'

Returns:

Type Description
EntityPattern

EntityPattern AST node

Raises:

Type Description
ValueError

If entity has neither _iid nor key attributes

Source code in type_bridge/models/entity.py
def get_match_pattern(self, var_name: str = "$e") -> EntityPattern:
    """Get an AST EntityPattern for matching this entity instance.

    Prefers IID-based matching when available (most precise).
    Falls back to @key attribute matching.

    Args:
        var_name: Variable name to use in the pattern

    Returns:
        EntityPattern AST node

    Raises:
        ValueError: If entity has neither _iid nor key attributes
    """
    from type_bridge.query.ast import EntityPattern

    type_name = self.get_type_name()
    constraints = self._build_identification_constraints()
    return EntityPattern(variable=var_name, type_name=type_name, constraints=constraints)

to_dict

to_dict(*, include=None, exclude=None, by_alias=False, exclude_unset=False)

Serialize the entity to a primitive dict.

Parameters:

Name Type Description Default
include set[str] | None

Optional set of field names to include.

None
exclude set[str] | None

Optional set of field names to exclude.

None
by_alias bool

When True, use attribute TypeQL names instead of Python field names.

False
exclude_unset bool

When True, omit fields that were never explicitly set.

False
Source code in type_bridge/models/entity.py
def to_dict(
    self,
    *,
    include: set[str] | None = None,
    exclude: set[str] | None = None,
    by_alias: bool = False,
    exclude_unset: bool = False,
) -> dict[str, Any]:
    """Serialize the entity to a primitive dict.

    Args:
        include: Optional set of field names to include.
        exclude: Optional set of field names to exclude.
        by_alias: When True, use attribute TypeQL names instead of Python field names.
        exclude_unset: When True, omit fields that were never explicitly set.
    """
    # Let Pydantic handle include/exclude/exclude_unset, then unwrap Attribute values.
    dumped = self.model_dump(
        include=include,
        exclude=exclude,
        by_alias=False,
        exclude_unset=exclude_unset,
    )

    attrs = self.get_all_attributes()
    result: dict[str, Any] = {}

    for field_name, raw_value in dumped.items():
        attr_info = attrs[field_name]
        key = attr_info.typ.get_attribute_name() if by_alias else field_name
        if by_alias and key in result and key != field_name:
            # Avoid collisions when multiple fields share the same attribute type
            key = field_name
        result[key] = self._unwrap_value(raw_value)

    return result

from_dict classmethod

from_dict(data, *, field_mapping=None, strict=True)

Construct an Entity from a plain dictionary.

Parameters:

Name Type Description Default
data dict[str, Any]

External data to hydrate the Entity.

required
field_mapping dict[str, str] | None

Optional mapping of external keys to internal field names.

None
strict bool

When True, raise on unknown fields; otherwise ignore them.

True
Source code in type_bridge/models/entity.py
@classmethod
def from_dict(
    cls,
    data: dict[str, Any],
    *,
    field_mapping: dict[str, str] | None = None,
    strict: bool = True,
) -> Self:
    """Construct an Entity from a plain dictionary.

    Args:
        data: External data to hydrate the Entity.
        field_mapping: Optional mapping of external keys to internal field names.
        strict: When True, raise on unknown fields; otherwise ignore them.
    """
    mapping = field_mapping or {}
    attrs = cls.get_all_attributes()
    alias_to_field = {info.typ.get_attribute_name(): name for name, info in attrs.items()}
    normalized: dict[str, Any] = {}

    for raw_key, raw_value in data.items():
        internal_key = mapping.get(raw_key, raw_key)
        if internal_key not in attrs and raw_key in alias_to_field:
            internal_key = alias_to_field[raw_key]

        if internal_key not in attrs:
            if strict:
                raise ValueError(f"Unknown field '{raw_key}' for {cls.__name__}")
            continue

        if raw_value is None:
            continue

        attr_info = attrs[internal_key]
        wrapped_value = cls._wrap_attribute_value(raw_value, attr_info)

        if wrapped_value is None:
            continue

        normalized[internal_key] = wrapped_value

    return cls(**normalized)

__repr__

__repr__()

Developer-friendly string representation of entity.

Source code in type_bridge/models/entity.py
def __repr__(self) -> str:
    """Developer-friendly string representation of entity."""
    field_strs = []
    for field_name in self._owned_attrs:
        value = getattr(self, field_name, None)
        if value is not None:
            field_strs.append(f"{field_name}={value!r}")
    return f"{self.__class__.__name__}({', '.join(field_strs)})"

__str__

__str__()

User-friendly string representation of entity.

Source code in type_bridge/models/entity.py
def __str__(self) -> str:
    """User-friendly string representation of entity."""
    # Extract key attributes first
    key_parts = []
    other_parts = []

    for field_name, attr_info in self._owned_attrs.items():
        value = getattr(self, field_name, None)
        if value is None:
            continue

        # Extract actual value from Attribute instance
        display_value = unwrap_attribute(value)

        # Format the field
        field_str = f"{field_name}={display_value}"

        # Separate key attributes
        if attr_info.flags.is_key:
            key_parts.append(field_str)
        else:
            other_parts.append(field_str)

    # Show key attributes first, then others
    all_parts = key_parts + other_parts

    if all_parts:
        return f"{self.get_type_name()}({', '.join(all_parts)})"
    else:
        return f"{self.get_type_name()}()"

Relation

Bases: TypeDBType

Base class for TypeDB relations with Pydantic validation.

Relations can own attributes and have role players. Use TypeFlags to configure type name and abstract status. Supertype is determined automatically from Python inheritance.

This class inherits from TypeDBType and Pydantic's BaseModel, providing: - Automatic validation of attribute values - JSON serialization/deserialization - Type checking and coercion - Field metadata via Pydantic's Field()

Example

class Position(String): pass

class Salary(Integer): pass

class Employment(Relation): flags = TypeFlags(name="employment")

employee: Role[Person] = Role("employee", Person)
employer: Role[Company] = Role("employer", Company)

position: Position
salary: Salary | None

__init_subclass__

__init_subclass__(**kwargs)

Initialize relation subclass.

Source code in type_bridge/models/relation.py
def __init_subclass__(cls, **kwargs: Any) -> None:
    """Initialize relation subclass."""
    super().__init_subclass__(**kwargs)
    logger.debug(f"Initializing Relation subclass: {cls.__name__}")

    from type_bridge.models.schema_scanner import SchemaScanner

    scanner = SchemaScanner(cls)
    cls._roles = scanner.scan_roles()
    cls._owned_attrs = scanner.scan_attributes(is_relation=True)

    from type_bridge.models.registry import ModelRegistry

    ModelRegistry.register_attribute_owners(cls)

__pydantic_init_subclass__ classmethod

__pydantic_init_subclass__(**kwargs)

Called by Pydantic after model class initialization.

This is the right place to restore Role descriptors because: 1. init_subclass runs before Pydantic's metaclass finishes 2. Pydantic removes Role instances from class dict during construction 3. pydantic_init_subclass runs after Pydantic's setup is complete

This restores Role descriptors so class-level access (Employment.employee) returns a RoleRef for type-safe query building.

Source code in type_bridge/models/relation.py
@classmethod
def __pydantic_init_subclass__(cls, **kwargs: Any) -> None:
    """Called by Pydantic after model class initialization.

    This is the right place to restore Role descriptors because:
    1. __init_subclass__ runs before Pydantic's metaclass finishes
    2. Pydantic removes Role instances from class __dict__ during construction
    3. __pydantic_init_subclass__ runs after Pydantic's setup is complete

    This restores Role descriptors so class-level access (Employment.employee)
    returns a RoleRef for type-safe query building.
    """
    super().__pydantic_init_subclass__(**kwargs)

    # Restore Role descriptors using type.__setattr__ to bypass any Pydantic interception
    for role_name, role in cls._roles.items():
        type.__setattr__(cls, role_name, role)

get_roles classmethod

get_roles()

Get all roles defined on this relation.

Returns:

Type Description
dict[str, Role]

Dictionary mapping role names to Role instances

Source code in type_bridge/models/relation.py
@classmethod
def get_roles(cls) -> dict[str, Role]:
    """Get all roles defined on this relation.

    Returns:
        Dictionary mapping role names to Role instances
    """
    return cls._roles

to_ast

to_ast(var='$r')

Generate AST InsertClause for this relation instance.

Parameters:

Name Type Description Default
var str

Variable name to use

'$r'

Returns:

Type Description
InsertClause

InsertClause containing statements

Source code in type_bridge/models/relation.py
def to_ast(self, var: str = "$r") -> InsertClause:
    """Generate AST InsertClause for this relation instance.

    Args:
        var: Variable name to use

    Returns:
        InsertClause containing statements
    """
    from type_bridge.query.ast import InsertClause, RelationStatement, Statement
    from type_bridge.query.ast import RolePlayer as AstRolePlayer

    type_name = self.get_type_name()

    # Build role players
    role_players_ast = []
    for role_name, role in self.__class__._roles.items():
        # Get the entity from the instance
        entity_or_list = self.__dict__.get(role_name)
        if entity_or_list is not None:
            # Normalize to list for uniform handling
            entities = entity_or_list if isinstance(entity_or_list, list) else [entity_or_list]
            for i, entity in enumerate(entities):
                # Generate unique variable name for each player
                # Note: This assumes the manager will bind these variables in a match clause
                # or they are already bound. For now, we generate the usage.
                var_name = f"{role_name}_{i}" if len(entities) > 1 else role_name

                # We assume variables are passed in or coordinated.
                # Since to_ast is called on the instance, we need a convention.
                # This implies the Manager/Compiler must coordinate variable names between Match and Insert.
                # For now, let's use a standard derived name format that the Manager can also replicate.
                player_var = f"${var_name}"

                role_players_ast.append(
                    AstRolePlayer(role=role.role_name, player_var=player_var)
                )

    # Collect attribute statements using shared helper from TypeDBType
    inline_attributes = self._build_attribute_statements(var)

    statements: list[Statement] = [
        RelationStatement(
            variable=var,
            type_name=type_name,
            role_players=role_players_ast,
            include_variable=False,  # TypeDB 3.x insert doesn't use variable for relations
            attributes=inline_attributes,
        )
    ]

    return InsertClause(statements=statements)

get_match_clause_info

get_match_clause_info(var_name='$r')

Get match clause info for this relation instance.

Prefers IID-based matching when available (most precise). Falls back to role player matching.

Parameters:

Name Type Description Default
var_name str

Variable name to use in the match clause

'$r'

Returns:

Type Description
MatchClauseInfo

MatchClauseInfo with the match clause and role player clauses

Raises:

Type Description
ValueError

If any role player cannot be identified

Source code in type_bridge/models/relation.py
def get_match_clause_info(self, var_name: str = "$r") -> MatchClauseInfo:
    """Get match clause info for this relation instance.

    Prefers IID-based matching when available (most precise).
    Falls back to role player matching.

    Args:
        var_name: Variable name to use in the match clause

    Returns:
        MatchClauseInfo with the match clause and role player clauses

    Raises:
        ValueError: If any role player cannot be identified
    """
    type_name = self.get_type_name()

    # Prefer IID-based matching when available
    relation_iid = getattr(self, "_iid", None)
    if relation_iid:
        main_clause = f"{var_name} isa {type_name}, iid {relation_iid}"
        return MatchClauseInfo(main_clause=main_clause, extra_clauses=[], var_name=var_name)

    # Fall back to role player matching
    roles = self.__class__._roles
    role_parts = []
    extra_clauses = []

    for role_name, role in roles.items():
        entity_or_list = self.__dict__.get(role_name)
        if entity_or_list is None:
            raise ValueError(f"Role player '{role_name}' is required for matching")

        # Normalize to list for uniform handling
        entities = entity_or_list if isinstance(entity_or_list, list) else [entity_or_list]

        for i, entity in enumerate(entities):
            player_var = f"${role_name}_{i}" if len(entities) > 1 else f"${role_name}"
            role_parts.append(f"{role.role_name}: {player_var}")

            # Get match clause for the role player entity
            player_match = entity.get_match_clause_info(player_var)
            extra_clauses.append(player_match.main_clause)
            extra_clauses.extend(player_match.extra_clauses)

    roles_str = ", ".join(role_parts)
    main_clause = f"{var_name} isa {type_name} ({roles_str})"

    return MatchClauseInfo(
        main_clause=main_clause, extra_clauses=extra_clauses, var_name=var_name
    )

get_match_patterns

get_match_patterns(var_name='$r')

Get AST patterns for matching this relation instance.

Returns a list of patterns: the main RelationPattern plus EntityPatterns for each role player (when matching by role players, not IID).

Parameters:

Name Type Description Default
var_name str

Variable name to use in the pattern

'$r'

Returns:

Type Description
list[Pattern]

List of Pattern AST nodes

Raises:

Type Description
ValueError

If any role player cannot be identified

Source code in type_bridge/models/relation.py
def get_match_patterns(self, var_name: str = "$r") -> list[Pattern]:
    """Get AST patterns for matching this relation instance.

    Returns a list of patterns: the main RelationPattern plus EntityPatterns
    for each role player (when matching by role players, not IID).

    Args:
        var_name: Variable name to use in the pattern

    Returns:
        List of Pattern AST nodes

    Raises:
        ValueError: If any role player cannot be identified
    """
    from type_bridge.query.ast import (
        IidConstraint,
        RelationPattern,
        RolePlayer,
    )

    type_name = self.get_type_name()
    patterns: list[Pattern] = []

    # Prefer IID-based matching when available
    relation_iid = getattr(self, "_iid", None)
    if relation_iid:
        main_pattern = RelationPattern(
            variable=var_name,
            type_name=type_name,
            role_players=[],
            constraints=[IidConstraint(iid=relation_iid)],
        )
        return [main_pattern]

    # Fall back to role player matching
    roles = self.__class__._roles
    role_player_nodes: list[RolePlayer] = []

    for role_name, role in roles.items():
        entity_or_list = self.__dict__.get(role_name)
        if entity_or_list is None:
            raise ValueError(f"Role player '{role_name}' is required for matching")

        # Normalize to list for uniform handling
        entities = entity_or_list if isinstance(entity_or_list, list) else [entity_or_list]

        for i, player in enumerate(entities):
            player_var = f"${role_name}_{i}" if len(entities) > 1 else f"${role_name}"
            role_player_nodes.append(RolePlayer(role=role.role_name, player_var=player_var))

            # Get AST pattern for the role player (could be Entity or Relation)
            if hasattr(player, "get_match_pattern"):
                # Entity: returns single pattern
                patterns.append(player.get_match_pattern(player_var))
            else:
                # Relation: returns list of patterns
                patterns.extend(player.get_match_patterns(player_var))

    # Build main relation pattern
    main_pattern = RelationPattern(
        variable=var_name,
        type_name=type_name,
        role_players=role_player_nodes,
        constraints=[],
    )
    # Insert main pattern first, then role player patterns
    patterns.insert(0, main_pattern)

    return patterns

to_schema_definition classmethod

to_schema_definition()

Generate TypeQL schema definition for this relation.

Returns:

Type Description
str | None

TypeQL schema definition string, or None if this is a base class

Source code in type_bridge/models/relation.py
@classmethod
def to_schema_definition(cls) -> str | None:
    """Generate TypeQL schema definition for this relation.

    Returns:
        TypeQL schema definition string, or None if this is a base class
    """
    from type_bridge.typeql.annotations import (
        format_card_annotation,
        format_type_annotations,
    )

    # Base classes don't appear in TypeDB schema
    if cls.is_base():
        return None

    type_name = cls.get_type_name()
    lines = []

    # Define relation type with supertype from Python inheritance
    # TypeDB 3.x syntax: relation name @abstract, sub parent,
    supertype = cls.get_supertype()
    type_annotations = format_type_annotations(abstract=cls.is_abstract())

    relation_def = f"relation {type_name}"
    if type_annotations:
        relation_def += " " + " ".join(type_annotations)
    if supertype:
        relation_def += f", sub {supertype}"

    lines.append(relation_def)

    # Add roles with optional cardinality constraints
    for role in cls._roles.values():
        role_def = f"    relates {role.role_name}"
        # Add cardinality annotation if not default (1..1)
        if role.cardinality is not None:
            card_annotation = format_card_annotation(role.cardinality.min, role.cardinality.max)
            if card_annotation:
                role_def += f" {card_annotation}"
        lines.append(role_def)

    # Add attribute ownerships using shared helper
    lines.extend(cls._build_owns_lines())

    # Join with commas, but end with semicolon (no comma before semicolon)
    return ",\n".join(lines) + ";"

__repr__

__repr__()

Developer-friendly string representation of relation.

Source code in type_bridge/models/relation.py
def __repr__(self) -> str:
    """Developer-friendly string representation of relation."""
    parts = []
    # Show role players
    for role_name in self._roles:
        player = getattr(self, role_name, None)
        if player is not None:
            parts.append(f"{role_name}={player!r}")
    # Show attributes
    for field_name in self._owned_attrs:
        value = getattr(self, field_name, None)
        if value is not None:
            parts.append(f"{field_name}={value!r}")
    return f"{self.__class__.__name__}({', '.join(parts)})"

__str__

__str__()

User-friendly string representation of relation.

Source code in type_bridge/models/relation.py
def __str__(self) -> str:
    """User-friendly string representation of relation."""
    parts = []

    # Show role players first (more important)
    role_parts = []
    for role_name, role in self._roles.items():
        player = getattr(self, role_name, None)
        # Only show role players that are actual entity instances (have _owned_attrs)
        if player is not None and hasattr(player, "_owned_attrs"):
            # Get a simple representation of the player (their key attribute)
            key_info = extract_entity_key(player)
            if key_info:
                _, _, raw_value = key_info
                role_parts.append(f"{role_name}={raw_value}")

    if role_parts:
        parts.append("(" + ", ".join(role_parts) + ")")

    # Show attributes
    attr_parts = []
    for field_name, attr_info in self._owned_attrs.items():
        value = getattr(self, field_name, None)
        if value is None:
            continue

        # Extract actual value from Attribute instance
        display_value = unwrap_attribute(value)

        attr_parts.append(f"{field_name}={display_value}")

    if attr_parts:
        parts.append("[" + ", ".join(attr_parts) + "]")

    if parts:
        return f"{self.get_type_name()}{' '.join(parts)}"
    else:
        return f"{self.get_type_name()}()"

Role

Role(role_name, player_type, *additional_player_types, cardinality=None)

Descriptor for relation role players with type safety.

Generic type T represents the type (Entity or Relation) that can play this role. TypeDB supports both entities and relations as role players.

Example

Entity as role player

class Employment(Relation): employee: Role[Person] = Role("employee", Person) employer: Role[Company] = Role("employer", Company)

Relation as role player

class Permission(Relation): permitted_subject: Role[Subject] = Role("permitted_subject", Subject) permitted_access: Role[Access] = Role("permitted_access", Access) # Access is a Relation

Initialize a role.

Parameters:

Name Type Description Default
role_name str

The name of the role in TypeDB

required
player_type type[T]

The type (Entity or Relation) that can play this role

required
additional_player_types type[T]

Optional additional types allowed to play this role

()
cardinality Card | None

Optional cardinality constraint for the role (e.g., Card(2, 2) for exactly 2)

None

Raises:

Type Description
ReservedWordError

If role_name is a TypeQL reserved word

TypeError

If player type is a library base class (Entity, Relation, TypeDBType)

Source code in type_bridge/models/role.py
def __init__(
    self,
    role_name: str,
    player_type: type[T],
    *additional_player_types: type[T],
    cardinality: Card | None = None,
):
    """Initialize a role.

    Args:
        role_name: The name of the role in TypeDB
        player_type: The type (Entity or Relation) that can play this role
        additional_player_types: Optional additional types allowed to play this role
        cardinality: Optional cardinality constraint for the role (e.g., Card(2, 2) for exactly 2)

    Raises:
        ReservedWordError: If role_name is a TypeQL reserved word
        TypeError: If player type is a library base class (Entity, Relation, TypeDBType)
    """
    # Validate role name doesn't conflict with TypeQL reserved words
    validate_reserved_word(role_name, "role")

    self.role_name = role_name
    self.cardinality = cardinality
    unique_types: list[type[T]] = []
    for typ in (player_type, *additional_player_types):
        # Validate that we're not using library base classes directly
        self._validate_player_type(typ)
        if typ not in unique_types:
            unique_types.append(typ)

    if not unique_types:
        # Should be impossible because player_type is required, but keeps type checkers happy
        raise ValueError("Role requires at least one player type")

    self.player_entity_types: tuple[type[T], ...] = tuple(unique_types)
    first_entity_type = unique_types[0]
    self.player_entity_type = first_entity_type
    # Get type name from the entity class(es)
    self.player_types = tuple(pt.get_type_name() for pt in self.player_entity_types)
    self.player_type = first_entity_type.get_type_name()
    self.attr_name: str | None = None

is_multi_player property

is_multi_player

Check if this role allows multiple players.

Returns True if cardinality allows more than one player (max > 1 or unbounded).

__set_name__

__set_name__(owner, name)

Called when role is assigned to a class.

Source code in type_bridge/models/role.py
def __set_name__(self, owner: type, name: str) -> None:
    """Called when role is assigned to a class."""
    self.attr_name = name

__get__

__get__(obj: None, objtype: type) -> RoleRef[T]
__get__(obj: Any, objtype: type) -> T
__get__(obj, objtype)

Get role player from instance or RoleRef from class.

When accessed from the class (obj is None), returns RoleRef for type-safe query building (e.g., Employment.employee.age.gt(Age(30))). When accessed from an instance, returns the entity playing the role.

Source code in type_bridge/models/role.py
def __get__(self, obj: Any, objtype: type) -> T | RoleRef[T]:
    """Get role player from instance or RoleRef from class.

    When accessed from the class (obj is None), returns RoleRef for
    type-safe query building (e.g., Employment.employee.age.gt(Age(30))).
    When accessed from an instance, returns the entity playing the role.
    """
    if obj is None:
        from type_bridge.fields.role import RoleRef

        return RoleRef(
            role_name=self.role_name,
            player_types=self.player_entity_types,
        )
    return obj.__dict__.get(self.attr_name)

__set__

__set__(obj, value)

Set role player(s) on instance.

For roles with cardinality > 1, accepts a list of entities. For single-player roles, accepts a single entity.

Source code in type_bridge/models/role.py
def __set__(self, obj: Any, value: T | list[T]) -> None:
    """Set role player(s) on instance.

    For roles with cardinality > 1, accepts a list of entities.
    For single-player roles, accepts a single entity.
    """
    if isinstance(value, list):
        # Multi-player role - validate each item in the list
        if not self.is_multi_player:
            raise TypeError(
                f"Role '{self.role_name}' does not allow multiple players. "
                f"Use cardinality=Card(...) to enable multi-player roles."
            )
        for item in value:
            if not isinstance(item, self.player_entity_types):
                allowed = ", ".join(pt.__name__ for pt in self.player_entity_types)
                raise TypeError(
                    f"Role '{self.role_name}' expects types ({allowed}), "
                    f"got {type(item).__name__} in list"
                )
        obj.__dict__[self.attr_name] = value
    else:
        # Single player
        if not isinstance(value, self.player_entity_types):
            allowed = ", ".join(pt.__name__ for pt in self.player_entity_types)
            raise TypeError(
                f"Role '{self.role_name}' expects types ({allowed}), got {type(value).__name__}"
            )
        obj.__dict__[self.attr_name] = value

multi classmethod

multi(role_name, player_type, *additional_player_types, cardinality=None)

Define a role playable by multiple entity types.

Parameters:

Name Type Description Default
role_name str

The name of the role in TypeDB

required
player_type type[T]

The first entity type that can play this role

required
additional_player_types type[T]

Additional entity types allowed to play this role

()
cardinality Card | None

Optional cardinality constraint for the role

None
Source code in type_bridge/models/role.py
@classmethod
def multi(
    cls,
    role_name: str,
    player_type: type[T],
    *additional_player_types: type[T],
    cardinality: Card | None = None,
) -> Role[T]:
    """Define a role playable by multiple entity types.

    Args:
        role_name: The name of the role in TypeDB
        player_type: The first entity type that can play this role
        additional_player_types: Additional entity types allowed to play this role
        cardinality: Optional cardinality constraint for the role
    """
    if len((player_type, *additional_player_types)) < 2:
        raise ValueError("Role.multi requires at least two player types")
    return cls(role_name, player_type, *additional_player_types, cardinality=cardinality)

__get_pydantic_core_schema__ classmethod

__get_pydantic_core_schema__(source_type, handler)

Define how Pydantic should validate Role fields.

Accepts either: - A single entity instance for single-player roles - A list of entity instances for multi-player roles (cardinality > 1)

Uses a custom validator that checks class names instead of isinstance, to handle generated code in different modules where the same class name exists but as a different Python object.

Source code in type_bridge/models/role.py
@classmethod
def __get_pydantic_core_schema__(
    cls, source_type: Any, handler: GetCoreSchemaHandler
) -> CoreSchema:
    """Define how Pydantic should validate Role fields.

    Accepts either:
    - A single entity instance for single-player roles
    - A list of entity instances for multi-player roles (cardinality > 1)

    Uses a custom validator that checks class names instead of isinstance,
    to handle generated code in different modules where the same class name
    exists but as a different Python object.
    """
    import types

    from pydantic_core import core_schema

    # Extract the entity type(s) from Role[T]
    # Handle both Role[Entity] and Role[Entity1 | Entity2] unions
    allowed_names: set[str] = set()

    if hasattr(source_type, "__args__") and source_type.__args__:
        for arg in source_type.__args__:
            # Check if it's a union type (e.g., Document | Email)
            if isinstance(arg, types.UnionType) or (
                hasattr(arg, "__origin__") and arg.__origin__ is type(int | str)
            ):
                # It's a union - get the individual types
                if hasattr(arg, "__args__"):
                    for union_arg in arg.__args__:
                        if hasattr(union_arg, "__name__"):
                            allowed_names.add(union_arg.__name__)
            elif hasattr(arg, "__name__"):
                allowed_names.add(arg.__name__)

    def validate_role_player(value: Any) -> Any:
        """Validate that value is an allowed entity type by class name.

        Checks the full inheritance chain (MRO) to support subclasses.
        E.g., if Document is allowed and Report is a subclass of Document,
        Report instances are accepted.
        """
        if not allowed_names:
            # No type constraints - allow anything
            return value

        def is_allowed_type(obj: Any) -> bool:
            """Check if obj's class or any base class matches allowed names."""
            # Check entire MRO (Method Resolution Order) for inheritance support
            for cls in type(obj).__mro__:
                if cls.__name__ in allowed_names:
                    return True
            return False

        if isinstance(value, list):
            # List of entities for multi-player roles
            for item in value:
                if not is_allowed_type(item):
                    raise ValueError(
                        f"Expected one of {allowed_names}, got {type(item).__name__}"
                    )
            return value
        else:
            # Single entity
            if not is_allowed_type(value):
                raise ValueError(f"Expected one of {allowed_names}, got {type(value).__name__}")
            return value

    return core_schema.no_info_plain_validator_function(validate_role_player)

FieldInfo dataclass

FieldInfo(attr_type=None, card_min=1, card_max=1, is_key=False, is_unique=False)

Information extracted from a field type annotation.

Attributes:

Name Type Description
attr_type type[Attribute] | None

The Attribute subclass (e.g., Name, Age)

card_min int | None

Minimum cardinality (None means use default)

card_max int | None

Maximum cardinality (None means unbounded)

is_key bool

Whether this field is marked as @key

is_unique bool

Whether this field is marked as @unique

MatchClauseInfo dataclass

MatchClauseInfo(main_clause, extra_clauses, var_name)

Information needed to build a TypeQL match clause for an instance.

Used by TypeDBManager to build CRUD queries for both entities and relations.

Attributes:

Name Type Description
main_clause str

The primary match clause (e.g., "$e isa person, has Name "Alice"")

extra_clauses list[str]

Additional match clauses (e.g., role player matches for relations)

var_name str

The main variable name (e.g., "$e" or "$r")

ModelAttrInfo dataclass

ModelAttrInfo(typ, flags)

Metadata for an attribute owned by an Entity or Relation.

Attributes:

Name Type Description
typ type[Attribute]

The Attribute subclass (e.g., Name, Age)

flags AttributeFlags

The AttributeFlags with key/unique/card annotations

WriteQueryInfo dataclass

WriteQueryInfo(match_clause, write_pattern)

Information needed to build a TypeQL write query for an instance.

Supports both entities (which only need insert/put pattern) and relations (which need match clause for role players + insert/put pattern).

Attributes:

Name Type Description
match_clause str | None

Optional match clause (for relations with role players)

write_pattern str

The insert/put pattern