fix(memory): add data integrity constraints to Fact model

- Change source_episode_ids from JSON to JSONB for PostgreSQL consistency
- Add unique constraint for global facts (project_id IS NULL)
- Add CHECK constraint ensuring reinforcement_count >= 1

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-05 17:39:30 +01:00
parent 74b8c65741
commit 33ec889fc4

View File

@@ -18,9 +18,8 @@ from sqlalchemy import (
Text, Text,
text, text,
) )
from sqlalchemy.dialects.postgresql import UUID as PGUUID from sqlalchemy.dialects.postgresql import JSONB, UUID as PGUUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.types import JSON
from app.models.base import Base, TimestampMixin, UUIDMixin from app.models.base import Base, TimestampMixin, UUIDMixin
@@ -61,8 +60,8 @@ class Fact(Base, UUIDMixin, TimestampMixin):
# Confidence score (0.0 to 1.0) # Confidence score (0.0 to 1.0)
confidence = Column(Float, nullable=False, default=0.8, index=True) confidence = Column(Float, nullable=False, default=0.8, index=True)
# Source tracking: which episodes contributed to this fact (stored as JSON array of UUID strings) # Source tracking: which episodes contributed to this fact (stored as JSONB array of UUID strings)
source_episode_ids: Column[list] = Column(JSON, default=list, nullable=False) source_episode_ids: Column[list] = Column(JSONB, default=list, nullable=False)
# Learning history # Learning history
first_learned = Column(DateTime(timezone=True), nullable=False) first_learned = Column(DateTime(timezone=True), nullable=False)
@@ -86,6 +85,15 @@ class Fact(Base, UUIDMixin, TimestampMixin):
unique=True, unique=True,
postgresql_where=text("project_id IS NOT NULL"), postgresql_where=text("project_id IS NOT NULL"),
), ),
# Unique constraint on triple for global facts (project_id IS NULL)
Index(
"ix_facts_unique_triple_global",
"subject",
"predicate",
"object",
unique=True,
postgresql_where=text("project_id IS NULL"),
),
# Query patterns # Query patterns
Index("ix_facts_subject_predicate", "subject", "predicate"), Index("ix_facts_subject_predicate", "subject", "predicate"),
Index("ix_facts_project_subject", "project_id", "subject"), Index("ix_facts_project_subject", "project_id", "subject"),
@@ -96,6 +104,10 @@ class Fact(Base, UUIDMixin, TimestampMixin):
"confidence >= 0.0 AND confidence <= 1.0", "confidence >= 0.0 AND confidence <= 1.0",
name="ck_facts_confidence_range", name="ck_facts_confidence_range",
), ),
CheckConstraint(
"reinforcement_count >= 1",
name="ck_facts_reinforcement_positive",
),
) )
def __repr__(self) -> str: def __repr__(self) -> str: