Skip to content

Commit e79a511

Browse files
feat: ManyToMany returns ManyToManyList with .add()/.remove()/.discard()
The list returned by ManyToMany.__get__ now supports incremental mutation via .add(entity), .remove(entity), and .discard(entity) methods that directly update reverse indexes without requiring full collection reassignment. Maintains backward compatibility with code that calls .add() on many-to-many relationship results. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 8723b78 commit e79a511

1 file changed

Lines changed: 73 additions & 1 deletion

File tree

ic_python_db/properties.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,78 @@ def __set__(self, obj, value):
526526
obj._save()
527527

528528

529+
class ManyToManyList(list):
530+
"""List subclass returned by ManyToMany.__get__ that supports .add() and .remove()."""
531+
532+
def __init__(self, items, owner, prop):
533+
super().__init__(items)
534+
self._owner = owner
535+
self._prop = prop
536+
537+
def add(self, entity):
538+
"""Add an entity to this many-to-many relationship."""
539+
from .db_engine import Database
540+
from .entity import Entity
541+
542+
if isinstance(entity, (str, int)):
543+
resolved = self._prop.resolve_entity(self._owner, entity)
544+
elif isinstance(entity, Entity):
545+
resolved = entity
546+
else:
547+
raise TypeError(f"Cannot add {type(entity)} to ManyToMany relation")
548+
549+
self._prop.validate_entity(resolved)
550+
db = Database.get_instance()
551+
552+
existing = db.reverse_index_get(
553+
self._owner._type, self._owner._id, self._prop.name
554+
)
555+
if resolved._id not in existing:
556+
db.reverse_index_add(
557+
self._owner._type, self._owner._id, self._prop.name, resolved._id
558+
)
559+
db.reverse_index_add(
560+
resolved._type, resolved._id, self._prop.reverse_name, self._owner._id
561+
)
562+
if not self._owner._do_not_save:
563+
self._owner._save()
564+
self.append(resolved)
565+
566+
def discard(self, entity):
567+
"""Remove an entity from this many-to-many relationship (no error if absent)."""
568+
from .db_engine import Database
569+
from .entity import Entity
570+
571+
if isinstance(entity, Entity):
572+
entity_id = entity._id
573+
entity_type = entity._type
574+
else:
575+
entity_id = str(entity)
576+
entity_type = None
577+
578+
db = Database.get_instance()
579+
db.reverse_index_remove(
580+
self._owner._type, self._owner._id, self._prop.name, entity_id
581+
)
582+
if entity_type:
583+
db.reverse_index_remove(
584+
entity_type, entity_id, self._prop.reverse_name, self._owner._id
585+
)
586+
else:
587+
for type_name in self._prop._get_allowed_types():
588+
db.reverse_index_remove(
589+
type_name, entity_id, self._prop.reverse_name, self._owner._id
590+
)
591+
592+
self[:] = [e for e in self if getattr(e, "_id", None) != entity_id]
593+
if not self._owner._do_not_save:
594+
self._owner._save()
595+
596+
def remove(self, entity):
597+
"""Remove an entity from this many-to-many relationship."""
598+
self.discard(entity)
599+
600+
529601
class ManyToMany(Relation[E]):
530602
"""Many-to-many relationship with bidirectional reverse indexes.
531603
@@ -562,7 +634,7 @@ def __get__(
562634
if entity:
563635
entities.append(entity)
564636
break
565-
return entities # type: ignore[return-value]
637+
return ManyToManyList(entities, obj, self) # type: ignore[return-value]
566638

567639
def __set__(self, obj, values):
568640
from .db_engine import Database

0 commit comments

Comments
 (0)