"""
|
FSM QuerySet Mixins for annotating entities with their current state.
|
|
Provides reusable Django QuerySet mixins that efficiently annotate entities
|
with their current FSM state using optimized subqueries to prevent N+1 queries.
|
|
Usage:
|
class TaskQuerySet(FSMStateQuerySetMixin, models.QuerySet):
|
pass
|
|
class TaskManager(models.Manager):
|
def get_queryset(self):
|
return TaskQuerySet(self.model, using=self._db)
|
|
Note:
|
State annotation is guarded by 'fflag_feat_fit_568_finite_state_management' only.
|
This allows background FSM processes to annotate current_state for internal use.
|
|
UI/API consumption of state fields is separately controlled by serializers
|
that check BOTH 'fflag_feat_fit_568_finite_state_management' AND
|
'fflag_feat_fit_710_fsm_state_fields' before exposing state data.
|
|
When the flag is disabled, no annotation is performed and there is zero performance impact.
|
"""
|
|
import logging
|
|
from core.current_request import CurrentContext
|
from core.feature_flags import flag_set
|
from django.db.models import OuterRef, Subquery
|
from fsm.registry import get_state_model
|
|
logger = logging.getLogger(__name__)
|
|
|
class FSMStateQuerySetMixin:
|
"""
|
Mixin for Django QuerySets to efficiently annotate FSM state.
|
|
Provides the `with_state()` method that adds a `current_state`
|
annotation to the queryset using an optimized subquery.
|
|
This approach:
|
- Prevents N+1 queries by using a single JOIN/subquery
|
- Handles missing states gracefully (returns None)
|
- Uses UUID7 natural ordering for optimal performance
|
- Works with any FSM entity that has a registered state model
|
|
Example:
|
# In your model manager
|
class TaskManager(models.Manager):
|
def get_queryset(self):
|
return TaskQuerySet(self.model, using=self._db)
|
|
def with_state(self):
|
return self.get_queryset().with_state()
|
|
# Usage - both approaches work identically
|
tasks = Task.objects.with_state().filter(project=project)
|
# Or chain it after filters
|
tasks = Task.objects.filter(project=project).with_state()
|
|
for task in tasks:
|
print(f"Task {task.id}: {task.current_state}") # No additional queries!
|
"""
|
|
def with_state(self):
|
"""
|
Annotate the queryset with the current FSM state.
|
|
Adds a `current_state` field to each object containing the current
|
state string value. This is done using an efficient subquery that
|
leverages UUID7 natural ordering to prevent N+1 queries.
|
|
Returns:
|
QuerySet: The annotated queryset with `current_state` field
|
|
Example:
|
# Chain after filters
|
tasks = Task.objects.filter(project=project).with_state()
|
|
# Or use from manager
|
tasks = Task.objects.with_state().filter(project=project)
|
|
# Multiple chaining
|
tasks = Task.objects.filter(is_labeled=True).with_state().order_by('-created_at')
|
|
Note:
|
- If FSM feature flag is disabled, returns queryset unchanged (zero impact)
|
- If no state exists for an entity, `current_state` will be None
|
- The state is read-only and should not be modified directly
|
"""
|
# Check only fflag_feat_fit_568_finite_state_management for background FSM processes.
|
# This allows background processes to annotate current_state for internal use.
|
# UI/API serializers separately check both fflag_feat_fit_568 AND fflag_feat_fit_710
|
# before exposing state data to consumers.
|
user = CurrentContext.get_user()
|
if not flag_set('fflag_feat_fit_568_finite_state_management', user=user):
|
logger.debug('FSM feature flag disabled, skipping state annotation')
|
return self
|
|
# Get the entity name from the model
|
entity_name = self.model._meta.model_name
|
|
# Get the state model for this entity
|
state_model = get_state_model(entity_name)
|
|
if not state_model:
|
# No state model registered, return queryset as-is
|
logger.debug(f'No state model registered for {entity_name}, skipping annotation')
|
return self
|
|
# Get the foreign key field name on the state model
|
# e.g., 'task_id' for TaskState
|
entity_field_name = state_model._get_entity_field_name()
|
fk_field = f'{entity_field_name}_id'
|
|
# Create subquery to get current state using UUID7 natural ordering
|
# This is extremely efficient because:
|
# 1. UUID7 provides natural time ordering (latest = highest ID)
|
# 2. We only fetch the state column, not the entire record
|
# 3. Django optimizes this into a single JOIN or lateral subquery
|
current_state_subquery = Subquery(
|
state_model.objects.filter(**{fk_field: OuterRef('pk')}).order_by('-id').values('state')[:1]
|
)
|
|
# Annotate the queryset with the current state
|
return self.annotate(current_state=current_state_subquery)
|