Cardinality¶
Complete reference for cardinality constraints and the Flag system in TypeBridge.
Overview¶
Cardinality defines how many values an attribute can have on an entity or relation. TypeBridge provides the Card API and Flag system for declaring cardinality constraints that map directly to TypeDB's @card annotations.
Card API¶
The Card class specifies minimum and maximum cardinality for attributes:
from type_bridge import Card
# Positional arguments
Card(min, max)
# Keyword arguments
Card(min=N) # At least N values
Card(max=N) # At most N values
Card(min=N, max=M) # Between N and M values
Card Constructors¶
from type_bridge import Card
# Exact count
Card(1, 1) # Exactly 1 → @card(1..1)
Card(2, 2) # Exactly 2 → @card(2..2)
# Minimum bound
Card(min=1) # At least 1 → @card(1..)
Card(min=2) # At least 2 → @card(2..)
# Maximum bound
Card(max=5) # At most 5 → @card(0..5)
Card(max=10) # At most 10 → @card(0..10)
# Range
Card(1, 5) # 1 to 5 → @card(1..5)
Card(2, 10) # 2 to 10 → @card(2..10)
Card(min=2, max=8) # 2 to 8 → @card(2..8)
Flag System¶
The Flag function combines cardinality with special annotations (Key, Unique):
from type_bridge import Flag, Key, Unique, Card
# Key attribute (implies @card(1..1))
field: Type = Flag(Key)
# Unique attribute (default @card(1..1))
field: Type = Flag(Unique)
# Multi-value with cardinality
field: list[Type] = Flag(Card(min=1))
# Key with multi-value
field: list[Type] = Flag(Key, Card(min=1))
Cardinality Patterns¶
TypeBridge provides multiple patterns for specifying cardinality:
Single-Value Attributes¶
Required Single Value (Default)¶
# Pattern: Type (no annotation needed)
# Cardinality: @card(1..1) - exactly one
name: Name # Required, exactly one
Optional Single Value¶
# Pattern: Type | None with explicit = None
# Cardinality: @card(0..1) - zero or one
age: Age | None = None # Optional, at most one
Multi-Value Attributes¶
Multi-value attributes must use list[Type] with Flag(Card(...)):
At Least N Values¶
# Pattern: list[Type] = Flag(Card(min=N))
# Cardinality: @card(N..) - at least N, unbounded
tags: list[Tag] = Flag(Card(min=1)) # @card(1..) - at least 1
admins: list[Admin] = Flag(Card(min=2)) # @card(2..) - at least 2
At Most N Values¶
# Pattern: list[Type] = Flag(Card(max=N))
# Cardinality: @card(0..N) - zero to N
emails: list[Email] = Flag(Card(max=3)) # @card(0..3) - up to 3
phones: list[Phone] = Flag(Card(max=5)) # @card(0..5) - up to 5
Range of Values¶
# Pattern: list[Type] = Flag(Card(min, max))
# Cardinality: @card(min..max) - min to max
jobs: list[Job] = Flag(Card(1, 5)) # @card(1..5) - 1 to 5
skills: list[Skill] = Flag(Card(min=2, max=10)) # @card(2..10) - 2 to 10
Zero or More Values¶
# Pattern: list[Type] = Flag(Card(min=0))
# Cardinality: @card(0..) - zero or more (unbounded)
tags: list[Tag] = Flag(Card(min=0)) # @card(0..)
Important: No Lists in TypeDB - Only Sets¶
TypeDB does not have a list type. Multi-value attributes are unordered sets. TypeBridge uses list[Type] syntax for convenience, but the order is never preserved.
# You write this in Python:
person = Person(tags=[Tag("python"), Tag("rust"), Tag("go")])
manager.insert(person)
# But TypeDB stores it as an unordered set
# When you fetch, order is NEVER guaranteed:
fetched = manager.get(name="Alice")[0]
# fetched.tags might be: [Tag("rust"), Tag("go"), Tag("python")]
# or any other order - it's completely unpredictable
Key points:
- TypeDB only has sets, not lists
- list[Type] is just Python syntax - internally it's a set
- Order is never preserved or guaranteed
- Order may differ between queries, restarts, or database operations
- Do not write code that depends on order
Special Annotations¶
Key Annotation¶
The Key annotation marks an attribute as a key (unique identifier):
from type_bridge import Flag, Key
class Person(Entity):
# Key implies @card(1..1) - required and unique
user_id: UserID = Flag(Key)
Generated TypeQL:
Properties:
- Implies @card(1..1) (exactly one)
- Enforces uniqueness across all instances
- Used for entity identification
Unique Annotation¶
The Unique annotation enforces uniqueness without making it a key:
from type_bridge import Flag, Unique
class Person(Entity):
email: Email = Flag(Unique) # Unique but not the primary key
Generated TypeQL:
Properties:
- Default @card(1..1) (exactly one)
- Enforces uniqueness across all instances
- Can have multiple unique attributes per entity
Combining Key/Unique with Card¶
from type_bridge import Flag, Key, Unique, Card
class Person(Entity):
# Key with multi-value
ids: list[ID] = Flag(Key, Card(min=1)) # @key @card(1..)
# Unique with custom cardinality
emails: list[Email] = Flag(Unique, Card(1, 3)) # @unique @card(1..3)
Cardinality Rules¶
Rule 1: Flag(Card(...)) Only with list[Type]¶
Multi-value attributes must use list[Type]:
# ✅ CORRECT: list[Type] with Card
tags: list[Tag] = Flag(Card(min=2))
# ❌ WRONG: Card without list[Type]
age: Age = Flag(Card(0, 1)) # Use Age | None instead!
Rule 2: list[Type] Must Have Flag(Card(...))¶
All multi-value attributes must specify cardinality:
# ✅ CORRECT: list[Type] with Card
tags: list[Tag] = Flag(Card(min=1))
# ❌ WRONG: list[Type] without Card
tags: list[Tag] # Error: missing cardinality!
# ❌ WRONG: Key/Unique alone is not enough
tags: list[Tag] = Flag(Key) # Error: need Card too!
tags: list[Tag] = Flag(Key, Card(min=1)) # ✅ CORRECT
Rule 3: Optional Single Values Use Type | None¶
For zero-or-one cardinality, use union types:
# ✅ CORRECT: Type | None for optional
age: Age | None = None # @card(0..1)
# ❌ WRONG: Don't use Card for single optional
age: Age = Flag(Card(0, 1))
Rule 4: Explicit = None for Optional Fields¶
Optional fields must have explicit defaults:
Complete Examples¶
Entity with Mixed Cardinality¶
from type_bridge import (
Entity, TypeFlags,
String, Integer, Boolean,
Flag, Key, Unique, Card
)
class UserID(String):
pass
class Username(String):
pass
class Email(String):
pass
class Age(Integer):
pass
class IsActive(Boolean):
pass
class Role(String):
pass
class Tag(String):
pass
class User(Entity):
flags = TypeFlags(name="user")
# Key: exactly one, unique
user_id: UserID = Flag(Key)
# Unique: exactly one, unique but not key
email: Email = Flag(Unique)
# Required: exactly one
username: Username
# Optional: zero or one
age: Age | None = None
is_active: IsActive | None = None
# Multi-value: at least one
roles: list[Role] = Flag(Card(min=1))
# Multi-value: zero to five
tags: list[Tag] = Flag(Card(max=5))
Generated TypeQL:
define
attribute user_id, value string;
attribute username, value string;
attribute email, value string;
attribute age, value integer;
attribute is_active, value boolean;
attribute role, value string;
attribute tag, value string;
entity user,
owns user_id @key,
owns email @unique,
owns username @card(1..1),
owns age @card(0..1),
owns is_active @card(0..1),
owns role @card(1..),
owns tag @card(0..5);
Relation with Cardinality¶
from type_bridge import Relation, TypeFlags, Role, Card
class Friendship(Relation):
flags = TypeFlags(name="friendship")
# Exactly 2 friends (symmetric relation)
friend: Role[Person] = Role("friend", Person, Card(2, 2))
# Optional attributes
since: StartDate | None = None
is_active: IsActive | None = None
# Multi-value attributes
shared_interests: list[Interest] = Flag(Card(min=0))
Generated TypeQL:
relation friendship,
relates friend @card(2..2),
owns since @card(0..1),
owns is_active @card(0..1),
owns shared_interests @card(0..);
Complex Cardinality Example¶
from type_bridge import Entity, TypeFlags, Flag, Key, Unique, Card
class Product(Entity):
flags = TypeFlags(name="product")
# Key: product_id (exactly one, unique)
product_id: ProductID = Flag(Key)
# Unique: SKU (exactly one, unique)
sku: SKU = Flag(Unique)
# Required: name (exactly one)
name: ProductName
# Optional: description (zero or one)
description: Description | None = None
# Multi-value: at least one category
categories: list[Category] = Flag(Card(min=1))
# Multi-value: 1 to 5 images
images: list[ImageURL] = Flag(Card(1, 5))
# Multi-value: 0 to 10 tags
tags: list[Tag] = Flag(Card(max=10))
# Multi-value: at least 2 suppliers
suppliers: list[SupplierID] = Flag(Card(min=2))
Generated TypeQL:
entity product,
owns product_id @key,
owns sku @unique,
owns name @card(1..1),
owns description @card(0..1),
owns category @card(1..),
owns image_url @card(1..5),
owns tag @card(0..10),
owns supplier_id @card(2..);
Cardinality Semantics¶
TypeBridge follows these cardinality semantics:
| Pattern | Annotation | Meaning |
|---|---|---|
Type |
@card(1..1) |
Exactly one (required) |
Type \| None |
@card(0..1) |
Zero or one (optional) |
Flag(Key) |
@key |
Exactly one, unique (implies @card(1..1)) |
Flag(Unique) |
@unique |
Exactly one, unique (default @card(1..1)) |
list[Type] = Flag(Card(min=N)) |
@card(N..) |
At least N, unbounded |
list[Type] = Flag(Card(max=N)) |
@card(0..N) |
Zero to N |
list[Type] = Flag(Card(min, max)) |
@card(min..max) |
Min to max |
Best Practices¶
1. Use Clear Cardinality Patterns¶
Follow the established patterns for clarity:
# ✅ GOOD: Clear patterns
name: Name # Required (1..1)
age: Age | None = None # Optional (0..1)
tags: list[Tag] = Flag(Card(min=1)) # Multi-value (1..)
# ❌ CONFUSING: Mixing patterns
name: Name | None = Name("default") # Don't provide default for optional
age: Age = Flag(Card(1, 1)) # Just use Age
2. Use Semantic Cardinality¶
Choose cardinality that reflects business logic:
# ✅ GOOD: Reflects reality
class Person(Entity):
# Everyone has exactly one birth date
birth_date: BirthDate
# Some people have a middle name, some don't
middle_name: MiddleName | None = None
# People can have multiple phone numbers
phones: list[Phone] = Flag(Card(min=0))
# ❌ POOR: Doesn't reflect reality
class Person(Entity):
birth_date: BirthDate | None = None # Everyone has a birth date!
middle_name: MiddleName # Not everyone has one
3. Use Key for Primary Identifiers¶
Always use Flag(Key) for primary identifiers:
# ✅ GOOD: Clear primary key
class User(Entity):
user_id: UserID = Flag(Key)
# ❌ POOR: Unique without Key
class User(Entity):
user_id: UserID = Flag(Unique) # Should be Key
4. Use Unique for Secondary Identifiers¶
Use Flag(Unique) for fields that must be unique but aren't the primary key:
class User(Entity):
user_id: UserID = Flag(Key) # Primary identifier
email: Email = Flag(Unique) # Secondary identifier
username: Username = Flag(Unique) # Secondary identifier
5. Remember: TypeDB Has No Lists - Only Sets¶
TypeDB only has sets. Multi-value attributes are always unordered:
# Python syntax uses list[Type], but it's a set in TypeDB
class Person(Entity):
tags: list[Tag] = Flag(Card(min=1)) # This is a SET, not a list!
# Order is NEVER preserved
person = manager.get(name="Alice")[0]
# person.tags order is unpredictable and may change
Key points:
- TypeDB has no list type - only unordered sets
- list[Type] is Python syntax only - it's a set underneath
- Never write code that depends on order
- Sort in application code if you need ordering temporarily
Deprecated APIs¶
The following cardinality APIs are deprecated:
# ❌ DEPRECATED: Cardinal, Min, Max, Range
from type_bridge import Cardinal, Min, Max, Range
tags: Min[1, Tag] # Use: list[Tag] = Flag(Card(min=1))
tags: Max[5, Tag] # Use: list[Tag] = Flag(Card(max=5))
tags: Range[1, 5, Tag] # Use: list[Tag] = Flag(Card(1, 5))
age: Optional[Age] # Use: Age | None
Use modern Card API and PEP 604 union syntax instead.
See Also¶
- Attributes - Attribute types that use cardinality
- Entities - Entity ownership with cardinality constraints
- Relations - Relations with role and attribute cardinality
- Validation - Type validation and cardinality enforcement