Relations¶
Complete reference for defining relations in TypeBridge.
Overview¶
Relations are first-class connections between entities in TypeDB. Unlike foreign keys in traditional databases, relations in TypeDB explicitly define role players and can own attributes themselves.
Relation Base Class¶
The Relation base class provides the foundation for all relation types:
from type_bridge import Relation
class Relation:
"""Base class for relations."""
@classmethod
def get_type_name(cls) -> str:
"""Returns type name from flags or lowercase class name."""
@classmethod
def get_supertype(cls) -> str | None:
"""Returns supertype from Python inheritance."""
@classmethod
def get_roles(cls) -> dict[str, Role]:
"""Returns mapping of role names to Role objects."""
@classmethod
def get_owned_attributes(cls) -> dict[str, ModelAttrInfo]:
"""Returns mapping of field names to attribute info."""
@classmethod
def to_schema_definition(cls) -> str:
"""Generates relation schema with role and ownership declarations."""
@classmethod
def manager(cls, db: Database) -> RelationManager:
"""Creates a type-safe CRUD manager for this relation."""
Basic Relation Definition¶
Define relations by inheriting from Relation and declaring roles and attribute ownership:
from type_bridge import Relation, TypeFlags, Role, String
# Define entities
class Person(Entity):
flags = TypeFlags(name="person")
name: Name = Flag(Key)
class Company(Entity):
flags = TypeFlags(name="company")
name: Name = Flag(Key)
# Define attribute for relation
class Position(String):
pass
# Define relation
class Employment(Relation):
flags = TypeFlags(name="employment")
# Role players
employee: Role[Person] = Role("employee", Person)
employer: Role[Company] = Role("employer", Company)
# Owned attributes
position: Position
Generated TypeQL:
define
attribute position, value string;
relation employment,
relates employee,
relates employer,
owns position @card(1..1);
person plays employment:employee;
company plays employment:employer;
Role Syntax¶
Roles are defined using the Role class with type parameters:
from type_bridge import Role
class Employment(Relation):
# Syntax: field_name: Role[EntityType] = Role("role_name", EntityType)
employee: Role[Person] = Role("employee", Person)
employer: Role[Company] = Role("employer", Company)
Components:
field_name: Python field name (e.g.,employee,employer)Role[EntityType]: Type hint for role player typeRole("role_name", EntityType): Role definition with TypeDB role name and player type
Type-Safe Role Access¶
New in v0.9.0: Access role player attributes with full type safety for query building.
Class-Level vs Instance-Level Access¶
Role fields behave differently at class-level and instance-level:
class Employment(Relation):
flags = TypeFlags(name="employment")
employee: Role[Person] = Role("employee", Person)
employer: Role[Company] = Role("employer", Company)
# Class-level access returns RoleRef for query building
Employment.employee # Returns RoleRef
Employment.employee.age # Returns RolePlayerNumericFieldRef
Employment.employee.name # Returns RolePlayerStringFieldRef
# Instance-level access returns actual entity instances
emp = Employment(employee=alice, employer=techcorp)
emp.employee # Returns Person instance (alice)
emp.employer # Returns Company instance (techcorp)
Role Player Field References¶
Access role player attributes through the RoleRef:
# Numeric field reference
age_ref = Employment.employee.age
# Type: RolePlayerNumericFieldRef
# String field reference
name_ref = Employment.employee.name
# Type: RolePlayerStringFieldRef
Query Building with Role Player Fields¶
Use role player field references for type-safe filtering:
manager = Employment.manager(db)
# Filter by role player attribute
results = manager.filter(
Employment.employee.age.gte(Age(30))
).execute()
# String operations
results = manager.filter(
Employment.employer.name.contains(Name("Tech"))
).execute()
# Combine multiple conditions
results = manager.filter(
Employment.employee.age.gt(Age(25)),
Employment.employer.name.like(Name("^Tech.*"))
).execute()
Available Methods¶
Numeric fields:
.gt(value),.lt(value),.gte(value),.lte(value).eq(value),.neq(value)
String fields:
.contains(value)- Substring match.like(value)- Regex pattern.regex(value)- Regex pattern (alias)
Introspecting Role Attributes¶
Use dir() to discover available attributes:
# List all available attributes on a role
print(dir(Employment.employee))
# ['name', 'age', 'email', ...]
# For multi-player roles, returns union of all player attributes
print(dir(Trace.origin))
# ['name', 'subject', 'sender', ...] # From Document and Email
Public Role Access¶
New in v0.9.0: Use get_roles() to access role definitions:
# Get all roles
roles = Employment.get_roles()
# Returns: {'employee': Role(...), 'employer': Role(...)}
# Access role properties
role = roles['employee']
print(role.role_name) # 'employee'
print(role.player_entity_types) # (Person,)
Multi-player Roles¶
A single role can be playable by multiple entity types without introducing an artificial supertype:
class Document(Entity):
flags = TypeFlags(name="document")
name: Name = Flag(Key)
class Email(Entity):
flags = TypeFlags(name="email")
name: Name = Flag(Key)
class Trace(Relation):
flags = TypeFlags(name="trace")
origin: Role[Document | Email] = Role.multi("origin", Document, Email)
- Type hint: Use a PEP 604 union for IDE/type-checker support (e.g.,
Role[Document | Email]). - API:
Role.multi("role_name", TypeA, TypeB, ...)requires at least two player types; otherwise it raisesValueError. - Runtime validation: Assigning an instance of a disallowed type raises
TypeError. - Schema emission: Generates multiple
playsentries for the single role:
CRUD and queries¶
- Filtering or deleting by a multi-player role uses the actual player’s key attributes and works across all allowed entity types.
- Role uniqueness rules still apply: keep role names distinct (TypeDB requirement).
TypeFlags Configuration¶
Configure relation metadata using TypeFlags:
from type_bridge import TypeFlags
class Employment(Relation):
flags = TypeFlags(
name="employment", # TypeDB type name (default: lowercase class name)
abstract=False, # Whether this is an abstract relation (default: False)
case="snake_case" # Type name case formatting (default: "snake_case")
)
Implicit TypeFlags¶
For simple relations, TypeFlags is automatically created if not specified:
# These are equivalent:
class Employment(Relation):
employee: Role[Person] = Role("employee", Person)
employer: Role[Company] = Role("employer", Company)
class Employment(Relation):
flags = TypeFlags() # Not necessary
employee: Role[Person] = Role("employee", Person)
employer: Role[Company] = Role("employer", Company)
When explicit flags ARE needed:
abstract=True- Abstract relationsbase=True- Python-only base classes- Custom
name- Override type name - Custom
case- Non-default case formatting
Relation with Attributes¶
Relations can own attributes just like entities:
from type_bridge import Relation, TypeFlags, Role, String, Integer, Date, Flag, Card
class Position(String):
pass
class Salary(Integer):
pass
class StartDate(Date):
pass
class Skill(String):
pass
class Employment(Relation):
flags = TypeFlags(name="employment")
# Roles
employee: Role[Person] = Role("employee", Person)
employer: Role[Company] = Role("employer", Company)
# Single-value attributes
position: Position # @card(1..1) - required
salary: Salary | None = None # @card(0..1) - optional
start_date: StartDate # @card(1..1) - required
# Multi-value attributes
skills: list[Skill] = Flag(Card(min=1)) # @card(1..) - at least one
Generated TypeQL:
define
attribute position, value string;
attribute salary, value integer;
attribute start_date, value date;
attribute skill, value string;
relation employment,
relates employee,
relates employer,
owns position @card(1..1),
owns salary @card(0..1),
owns start_date @card(1..1),
owns skill @card(1..);
person plays employment:employee;
company plays employment:employer;
Role Cardinality¶
Specify how many times an entity can play a role:
from type_bridge import Relation, TypeFlags, Role, Card
class Friendship(Relation):
flags = TypeFlags(name="friendship")
# Both friends must be Person, exactly 2 role players
friend: Role[Person] = Role("friend", Person, Card(2, 2))
Generated TypeQL:
Python Inheritance for Relations¶
Relations support inheritance just like entities:
Basic Relation Inheritance¶
from type_bridge import Relation, TypeFlags, Role
class SocialRelation(Relation):
flags = TypeFlags(name="social-relation", abstract=True)
# Abstract role (will be overridden in subclasses)
related: Role[Person] = Role("related", Person, Card(2))
class Friendship(SocialRelation):
flags = TypeFlags(name="friendship")
# Override role with specific semantics
friend: Role[Person] = Role("friend", Person, Card(2))
Generated TypeQL:
relation social-relation @abstract,
relates related @card(2);
relation friendship, sub social-relation,
relates friend as related @card(2);
person plays social-relation:related;
person plays friendship:friend;
Role Specialization¶
Subrelations can specialize roles from parent relations:
class Employment(Relation):
flags = TypeFlags(name="employment", abstract=True)
employee: Role[Person] = Role("employee", Person)
employer: Role[Entity] = Role("employer", Entity) # Generic employer
class CompanyEmployment(Employment):
flags = TypeFlags(name="company-employment")
# Specialize employer role to only Company entities
employer: Role[Company] = Role("employer", Company)
Abstract Relations¶
Abstract relations cannot be instantiated directly but serve as base types:
from type_bridge import Relation, TypeFlags, Role
class Relation(Relation):
flags = TypeFlags(name="relation", abstract=True)
related: Role[Entity] = Role("related", Entity, Card(2))
class Friendship(Relation):
flags = TypeFlags(name="friendship")
friend: Role[Person] = Role("friend", Person, Card(2))
Generated TypeQL:
relation relation @abstract,
relates related @card(2);
relation friendship, sub relation,
relates friend as related @card(2);
Creating Relation Instances¶
Relations use keyword-only arguments for type safety:
from datetime import date
# Create entity instances
alice = Person(name=Name("Alice"))
techcorp = Company(name=Name("TechCorp"))
# ✅ CORRECT: Keyword arguments with entity instances
employment = Employment(
employee=alice,
employer=techcorp,
position=Position("Senior Engineer"),
salary=Salary(120000),
start_date=StartDate(date(2024, 1, 15)),
skills=[Skill("Python"), Skill("TypeDB"), Skill("FastAPI")]
)
# ❌ WRONG: Positional arguments
employment = Employment(alice, techcorp, Position("Engineer")) # Type error!
Complete Example¶
from type_bridge import (
Entity, Relation, TypeFlags, Role,
String, Integer, Date, Boolean,
Flag, Key, Unique, Card
)
from datetime import date
# Define attribute types
class Name(String):
pass
class Email(String):
pass
class CompanyID(String):
pass
class Position(String):
pass
class Salary(Integer):
pass
class StartDate(Date):
pass
class IsActive(Boolean):
pass
class Benefit(String):
pass
# Define entities
class Person(Entity):
flags = TypeFlags(name="person")
name: Name = Flag(Key)
email: Email = Flag(Unique)
class Company(Entity):
flags = TypeFlags(name="company")
company_id: CompanyID = Flag(Key)
name: Name
# Define relation
class Employment(Relation):
flags = TypeFlags(name="employment")
# Roles
employee: Role[Person] = Role("employee", Person)
employer: Role[Company] = Role("employer", Company)
# Single-value attributes
position: Position
salary: Salary
start_date: StartDate
is_active: IsActive | None = None
# Multi-value attributes
benefits: list[Benefit] = Flag(Card(min=0))
# Create instances
alice = Person(
name=Name("Alice Johnson"),
email=Email("alice@example.com")
)
techcorp = Company(
company_id=CompanyID("C001"),
name=Name("TechCorp Inc.")
)
employment = Employment(
employee=alice,
employer=techcorp,
position=Position("Senior Software Engineer"),
salary=Salary(120000),
start_date=StartDate(date(2024, 1, 15)),
is_active=IsActive(True),
benefits=[
Benefit("Health Insurance"),
Benefit("401k Match"),
Benefit("Remote Work")
]
)
Generated TypeQL:
define
attribute name, value string;
attribute email, value string;
attribute company_id, value string;
attribute position, value string;
attribute salary, value integer;
attribute start_date, value date;
attribute is_active, value boolean;
attribute benefit, value string;
entity person,
owns name @key,
owns email @unique;
entity company,
owns company_id @key,
owns name @card(1..1);
relation employment,
relates employee,
relates employer,
owns position @card(1..1),
owns salary @card(1..1),
owns start_date @card(1..1),
owns is_active @card(0..1),
owns benefit @card(0..);
person plays employment:employee;
company plays employment:employer;
Abstract Entity Types in Role Definitions¶
Relations can use abstract entity types in role definitions, allowing any subtype to play the role:
from type_bridge import Entity, Relation, TypeFlags, Role
# Abstract base entity
class Content(Entity):
flags = TypeFlags(name="content", abstract=True)
title: Title
# Concrete subtypes
class Article(Content):
flags = TypeFlags(name="article")
body: Body
class Video(Content):
flags = TypeFlags(name="video")
url: URL
# Relation using abstract type
class Authorship(Relation):
flags = TypeFlags(name="authorship")
author: Role[Person] = Role("author", Person)
content: Role[Content] = Role("content", Content) # Abstract type!
# Usage - any Content subtype can play the role
article = Article(title=Title("Python Guide"))
video = Video(title=Title("TypeDB Tutorial"), url=URL("https://..."))
author = Person(name=Name("Alice"))
# Both work because Article and Video inherit from Content
article_authorship = Authorship(author=author, content=article)
video_authorship = Authorship(author=author, content=video)
Generated TypeQL:
relation authorship,
relates author,
relates content;
person plays authorship:author;
content plays authorship:content; # Abstract type in role player definition
Polymorphic Role Player Type Resolution¶
When querying relations with polymorphic role players, TypeBridge resolves each role player to its concrete type, not the abstract declared type. This enables type-safe access to subtype-specific attributes:
# Query authorships - role players are resolved to concrete types
authorships = Authorship.manager(db).all()
for authorship in authorships:
content = authorship.content
# content is Article, Video, or other concrete subtype - NOT abstract Content
if isinstance(content, Article):
print(f"Article body: {content.body}")
elif isinstance(content, Video):
print(f"Video URL: {content.url}")
# Common attributes from abstract type are always accessible
print(f"Title: {content.title}")
How it works:
TypeBridge uses TypeDB's label() built-in function to fetch the actual type of each role player, then resolves it to the correct Python class. This happens automatically in:
RelationManager.get()- filter-based queriesRelationManager.all()- fetch all relationsRelationManager.get_by_iid()- IID-based lookupRelationQuery.execute()- chainable query execution
Benefits:
- Access concrete type-specific attributes directly
- Use
isinstance()for type-safe branching - Proper Python type inference in IDEs
- No need to manually resolve types from IIDs
Best Practices¶
1. Use Descriptive Role Names¶
Choose role names that clearly describe the relationship:
# ✅ GOOD: Clear role names
class Employment(Relation):
employee: Role[Person] = Role("employee", Person)
employer: Role[Company] = Role("employer", Company)
# ❌ POOR: Generic role names
class Employment(Relation):
person: Role[Person] = Role("person", Person)
company: Role[Company] = Role("company", Company)
2. Match Field Name to Role Name¶
For consistency, use the same name for the field and role:
# ✅ GOOD: Consistent naming
employee: Role[Person] = Role("employee", Person)
# ⚠️ CONFUSING: Different names
emp: Role[Person] = Role("employee", Person)
3. Use Abstract Relations for Hierarchies¶
Create abstract base relations for common patterns:
class SocialRelation(Relation):
flags = TypeFlags(abstract=True)
related: Role[Person] = Role("related", Person, Card(2))
class Friendship(SocialRelation):
friend: Role[Person] = Role("friend", Person, Card(2))
class Partnership(SocialRelation):
partner: Role[Person] = Role("partner", Person, Card(2))
4. Explicit Defaults for Optional Attributes¶
Always provide = None for optional attributes on relations:
See Also¶
- Entities - How to define entities that play roles
- Attributes - Attributes that relations can own
- Abstract Types - Working with abstract relations and polymorphic roles
- Cardinality - Cardinality constraints for roles and attributes
- CRUD Operations - Working with relations in the database
- Queries - Querying relations and role players