db: enforce unique constraint on test_detection_results

- Add UniqueConstraint(test_id, detection_rule_id) named uq_tdr_test_rule to TestDetectionResult model

- Alembic b025: safely deduplicate existing rows before creating constraint
This commit is contained in:
2026-02-18 13:20:28 +01:00
parent 6147abc87a
commit 55dba1e00a
2 changed files with 43 additions and 1 deletions

View File

@@ -0,0 +1,41 @@
"""add_unique_test_detection_result
Enforce one evaluation per (test, detection_rule) pair. Before creating
the constraint, duplicate rows (if any) are collapsed so the migration
never fails on an existing database.
Revision ID: b025uqtdr
Revises: b024critidx
Create Date: 2026-02-18 14:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
revision: str = "b025uqtdr"
down_revision: Union[str, None] = "b024critidx"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Remove duplicates keeping the most recently evaluated row
op.execute("""
DELETE FROM test_detection_results
WHERE id NOT IN (
SELECT DISTINCT ON (test_id, detection_rule_id) id
FROM test_detection_results
ORDER BY test_id, detection_rule_id, evaluated_at DESC NULLS LAST
)
""")
op.create_unique_constraint(
"uq_tdr_test_rule",
"test_detection_results",
["test_id", "detection_rule_id"],
)
def downgrade() -> None:
op.drop_constraint("uq_tdr_test_rule", "test_detection_results", type_="unique")

View File

@@ -7,7 +7,7 @@ rule as triggered / not triggered / not applicable, along with notes.
import uuid import uuid
from datetime import datetime from datetime import datetime
from sqlalchemy import Column, String, Text, Boolean, DateTime, ForeignKey, Index from sqlalchemy import Column, String, Text, Boolean, DateTime, ForeignKey, Index, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@@ -52,4 +52,5 @@ class TestDetectionResult(Base):
__table_args__ = ( __table_args__ = (
Index('ix_tdr_test', 'test_id'), Index('ix_tdr_test', 'test_id'),
Index('ix_tdr_rule', 'detection_rule_id'), Index('ix_tdr_rule', 'detection_rule_id'),
UniqueConstraint('test_id', 'detection_rule_id', name='uq_tdr_test_rule'),
) )