From 33ec889fc448f962a08f05190e4088eefd8a43a3 Mon Sep 17 00:00:00 2001 From: Felipe Cardoso Date: Mon, 5 Jan 2026 17:39:30 +0100 Subject: [PATCH] fix(memory): add data integrity constraints to Fact model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- backend/app/models/memory/fact.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/backend/app/models/memory/fact.py b/backend/app/models/memory/fact.py index 59aeb55..c7875a7 100644 --- a/backend/app/models/memory/fact.py +++ b/backend/app/models/memory/fact.py @@ -18,9 +18,8 @@ from sqlalchemy import ( 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.types import JSON 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 = 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_episode_ids: Column[list] = Column(JSON, default=list, nullable=False) + # Source tracking: which episodes contributed to this fact (stored as JSONB array of UUID strings) + source_episode_ids: Column[list] = Column(JSONB, default=list, nullable=False) # Learning history first_learned = Column(DateTime(timezone=True), nullable=False) @@ -86,6 +85,15 @@ class Fact(Base, UUIDMixin, TimestampMixin): unique=True, 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 Index("ix_facts_subject_predicate", "subject", "predicate"), Index("ix_facts_project_subject", "project_id", "subject"), @@ -96,6 +104,10 @@ class Fact(Base, UUIDMixin, TimestampMixin): "confidence >= 0.0 AND confidence <= 1.0", name="ck_facts_confidence_range", ), + CheckConstraint( + "reinforcement_count >= 1", + name="ck_facts_reinforcement_positive", + ), ) def __repr__(self) -> str: