Skip to content

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:

entity person,
    owns user_id @key;

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:

entity person,
    owns email @unique;

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:

# ✅ CORRECT: Explicit default
age: Age | None = None

# ❌ WRONG: Missing default
age: Age | None

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