"""Coverage snapshot models — periodic snapshots of coverage state. CoverageSnapshot stores aggregate metrics at a point in time. SnapshotTechniqueState stores per-technique state (normalized, one row per technique per snapshot) to avoid bloated JSONB fields. """ import uuid from sqlalchemy import ( Column, String, Float, Integer, DateTime, ForeignKey, Index, func, ) from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship from app.database import Base class CoverageSnapshot(Base): """A point-in-time snapshot of the organisation's overall coverage.""" __tablename__ = "coverage_snapshots" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) name = Column(String, nullable=True) # e.g. "Pre-remediación Q1" organization_score = Column(Float, nullable=False) total_techniques = Column(Integer, nullable=False) validated_count = Column(Integer, nullable=False) partial_count = Column(Integer, nullable=False) not_covered_count = Column(Integer, nullable=False) in_progress_count = Column(Integer, nullable=False) not_evaluated_count = Column(Integer, nullable=False) created_by = Column( UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, ) created_at = Column(DateTime(timezone=True), server_default=func.now()) # Relationships creator = relationship("User", foreign_keys=[created_by]) technique_states = relationship( "SnapshotTechniqueState", back_populates="snapshot", cascade="all, delete-orphan", ) class SnapshotTechniqueState(Base): """Per-technique state within a snapshot (normalised storage).""" __tablename__ = "snapshot_technique_states" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) snapshot_id = Column( UUID(as_uuid=True), ForeignKey("coverage_snapshots.id", ondelete="CASCADE"), nullable=False, ) technique_id = Column( UUID(as_uuid=True), ForeignKey("techniques.id", ondelete="CASCADE"), nullable=False, ) mitre_id = Column(String, nullable=False) # denormalised for fast queries status = Column(String, nullable=False) score = Column(Float, nullable=True) # Relationships snapshot = relationship("CoverageSnapshot", back_populates="technique_states") technique = relationship("Technique") __table_args__ = ( Index("ix_snapshot_technique_states_snapshot", "snapshot_id"), Index("ix_snapshot_technique_states_technique", "technique_id"), )