Models and Database
Gyrinx uses Django's ORM with PostgreSQL as the database. The application is structured around four main apps, each with distinct responsibilities and model relationships.
Database Architecture
Base Models
All application models inherit from AppBase
, which provides:
# gyrinx/core/models/base.py
class AppBase(HistoryMixin, Base, Owned, Archived):
objects = HistoryAwareManager()
This inheritance chain provides:
UUID primary keys (
Base
)Owner tracking (
Owned
) - links models to usersArchive functionality (
Archived
) - soft delete capabilityHistory tracking (
HistoryMixin
) - audit trailCustom manager (
HistoryAwareManager
) - enhanced operations
Core Model Relationships
Content App Models
The content app contains the game's static data - official Necromunda rules and equipment.
Core Content Models
ContentHouse
Represents Necromunda gangs/houses (Goliath, Escher, etc.)
Links to available fighters, equipment, and house-specific rules
ContentFighter
Base fighter templates (Leader, Champion, Ganger, etc.)
Defines stats, equipment options, and advancement paths
Links to house and available equipment lists
ContentEquipment
All equipment items (weapons, armor, gear)
Complex relationship system with profiles and upgrades
Cost calculations based on fighter type and house
ContentWeaponProfile
Weapon statistics (range, strength, damage, etc.)
Multiple profiles per weapon (different ammo types)
Cost modifiers and availability restrictions
Supporting Models
ContentSkill - Available skills and their effects
ContentRule - Game rules and special abilities
ContentPsykerPower - Psychic powers and disciplines
ContentEquipmentUpgrade - Weapon modifications and cyberteknika
ContentWeaponAccessory - Weapon attachments (scopes, etc.)
Core App Models
The core app handles user-created content - lists, gangs, and campaigns.
List Building Models
List
User-created gang lists for planning and experimentation
Links to a ContentHouse for validation and options
Cost calculations and equipment assignments
ListFighter
Instance of a ContentFighter within a user's list
Can override base costs and stats
Manages equipment assignments and skill selections
ListFighterEquipmentAssignment
Links a ListFighter to ContentEquipment
Handles cost overrides, weapon profiles, and accessories
Supports complex equipment configurations
Campaign Models
Campaign
Container for narrative campaigns with multiple participating gangs
Three status phases: pre-campaign, in-progress, post-campaign
Tracks participating lists, narrative, and summary
When started, clones all participating lists for campaign-specific tracking
CampaignAction
Records player actions during campaigns
Supports optional dice rolls (D6)
Tracks outcomes and results
Immutable audit log of campaign events
CampaignAssetType
Defines types of assets (e.g., Territory, Relic)
Singular and plural naming
Rich text descriptions
Campaign-specific configuration
CampaignAsset
Individual assets that can be held by gangs
Links to asset type and current holder
Supports transfers between gangs
Full history tracking
CampaignResourceType
Defines countable resources (e.g., Credits, Ammunition)
Default amounts allocated on campaign start
Rich text descriptions
CampaignListResource
Tracks resource amounts for each gang
Supports modifications with validation
Cannot go below zero
Creates action log entries on changes
Virtual Models
Gyrinx uses "virtual" models to abstract over different data sources:
class VirtualListFighterEquipmentAssignment:
"""Represents equipment assignment from either:
- Direct assignment (ListFighterEquipmentAssignment)
- Default assignment (ContentFighterDefaultAssignment)
- No assignment (for editing forms)
"""
This pattern allows the UI to treat all equipment consistently regardless of source.
Cost Calculation System
Gyrinx implements a layered cost calculation system:
Fighter Cost
Base ContentFighter cost
House-specific overrides
Manual user overrides
Sum of equipment assignment costs
Equipment Cost
Base equipment cost (from equipment list or trading post)
Weapon profile costs (special ammo)
Accessory costs (scopes, etc.)
Upgrade costs (cyberteknika)
Implementation
# Costs are calculated dynamically via annotations
fighters = ListFighter.objects.with_calculated_cost()
equipment = ContentEquipment.objects.with_cost_for_fighter(fighter)
Database Migrations
Development Workflow
# Create migration after model changes
manage makemigrations content -n "descriptive_migration_name"
# Create empty migration for data changes
manage makemigrations --empty content
# Apply migrations
manage migrate
# Check for issues
./scripts/check_migrations.sh
Migration Best Practices
Use descriptive names for migrations
Test migrations on production data dumps
Consider migration dependencies between apps
Use data migrations for complex transformations
Performance Considerations
Query Optimization
Use
select_related()
for foreign keysUse
prefetch_related()
for many-to-many relationshipsAnnotate calculated fields at the database level
Cache expensive computations
Database Indexes
Key indexes are automatically created for:
Foreign key relationships
UUID primary keys
History tracking fields
Frequently queried fields
Example Optimized Queries
# Efficient list loading with all relationships
lists = List.objects.select_related(
'content_house', 'owner'
).prefetch_related(
'listfighter_set__content_fighter',
'listfighter_set__equipment'
)
# Annotated cost calculations
fighters = ListFighter.objects.annotate(
total_cost=F('base_cost') + F('equipment_cost')
)
Testing Database Models
Model Tests
@pytest.mark.django_db
def test_list_fighter_cost_calculation():
house = ContentHouse.objects.create(name="Test House")
fighter = ContentFighter.objects.create(
name="Test Fighter",
cost=100,
house=house
)
list_fighter = ListFighter.objects.create(
content_fighter=fighter,
list=test_list
)
assert list_fighter.calculated_cost == 100
Migration Tests
def test_migration_preserves_data():
# Test that migrations don't lose or corrupt data
# Particularly important for complex schema changes
Schema Evolution
Adding New Models
Create model in appropriate app
Inherit from
AppBase
for standard functionalityAdd relationships carefully to avoid circular imports
Create migration and test with existing data
Modifying Existing Models
Consider backward compatibility
Use database migrations for schema changes
Use data migrations for data transformations
Test on production data dump
Deprecating Models
Mark as deprecated in docstrings
Remove from admin and forms first
Keep model for historical data access
Eventually remove with major version bump
Last updated