Skip to content

Commit 391feb6

Browse files
committed
add delete cascade to entity to delete search_index (postgres only)
Signed-off-by: phernandez <paul@basicmachines.co>
1 parent a920a9f commit 391feb6

3 files changed

Lines changed: 60 additions & 5 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Add cascade delete FK from search_index to entity
2+
3+
Revision ID: a2b3c4d5e6f7
4+
Revises: f8a9b2c3d4e5
5+
Create Date: 2025-12-02 07:00:00.000000
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
from alembic import op
12+
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = "a2b3c4d5e6f7"
16+
down_revision: Union[str, None] = "f8a9b2c3d4e5"
17+
branch_labels: Union[str, Sequence[str], None] = None
18+
depends_on: Union[str, Sequence[str], None] = None
19+
20+
21+
def upgrade() -> None:
22+
"""Add FK with CASCADE delete from search_index.entity_id to entity.id.
23+
24+
This migration is Postgres-only because:
25+
- SQLite uses FTS5 virtual tables which don't support foreign keys
26+
- The FK enables automatic cleanup of search_index entries when entities are deleted
27+
"""
28+
connection = op.get_bind()
29+
dialect = connection.dialect.name
30+
31+
if dialect == "postgresql":
32+
# First, clean up any orphaned search_index entries where entity no longer exists
33+
op.execute("""
34+
DELETE FROM search_index
35+
WHERE entity_id IS NOT NULL
36+
AND entity_id NOT IN (SELECT id FROM entity)
37+
""")
38+
39+
# Add FK with CASCADE - nullable FK allows search_index entries without entity_id
40+
op.create_foreign_key(
41+
"fk_search_index_entity_id",
42+
"search_index",
43+
"entity",
44+
["entity_id"],
45+
["id"],
46+
ondelete="CASCADE",
47+
)
48+
49+
50+
def downgrade() -> None:
51+
"""Remove the FK constraint."""
52+
connection = op.get_bind()
53+
dialect = connection.dialect.name
54+
55+
if dialect == "postgresql":
56+
op.drop_constraint("fk_search_index_entity_id", "search_index", type_="foreignkey")

src/basic_memory/models/search.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Search models and tables."""
22

3-
from sqlalchemy import DDL, Column, Integer, String, DateTime, Text
3+
from sqlalchemy import DDL, Column, Integer, String, DateTime, Text, ForeignKey
44
from sqlalchemy.dialects.postgresql import JSONB
55
from sqlalchemy.types import JSON
66

@@ -36,7 +36,9 @@ class SearchIndex(Base):
3636
relation_type = Column(String(100), nullable=True)
3737

3838
# Observation fields
39-
entity_id = Column(Integer, nullable=True)
39+
# Note: FK with CASCADE only applies to Postgres. SQLite uses FTS5 virtual tables
40+
# which don't support foreign keys, so cascade delete is handled explicitly there.
41+
entity_id = Column(Integer, ForeignKey("entity.id", ondelete="CASCADE"), nullable=True)
4042
category = Column(String(100), nullable=True)
4143

4244
# Common fields

test-int/test_db_wal_mode.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
"""
66

77
import pytest
8-
from unittest.mock import patch
98
from sqlalchemy import text
109

1110

@@ -142,8 +141,6 @@ async def test_null_pool_on_windows(tmp_path, monkeypatch):
142141
assert isinstance(engine.pool, NullPool)
143142

144143

145-
146-
147144
@pytest.mark.asyncio
148145
@pytest.mark.windows
149146
@pytest.mark.skipif(

0 commit comments

Comments
 (0)