Skip to content

Commit cb4ac17

Browse files
committed
RDBC-1023: add session.advanced.what_changed_for(entity) and get_tracked_entities()
C# Advanced.WhatChangedFor(entity) returns DocumentsChanges[] for a single entity. Python had no equivalent. Added _what_changed_for() to InMemoryDocumentSessionOperations and exposed it via Advanced.what_changed_for(). Matches C# behaviour: returns [DocumentDeleted] immediately when the entity has been deleted in the same session, before running the JSON diff. C# Advanced.GetTrackedEntities() returns a dict of all entities currently tracked by the session (stored and deleted). Added _get_tracked_entities() to InMemoryDocumentSessionOperations and exposed it via Advanced.get_tracked_entities(). Each entry maps document ID to a dict with keys: id, entity, is_deleted. Matches C# behaviour: entries deleted by string ID (present in _known_missing_ids but absent from _deleted_entities) appear with is_deleted=True. Regression tests: test_session_advanced_api.py verifies that what_changed_for() returns per-entity changes and correctly reports DocumentDeleted when the entity is deleted before the call; get_tracked_entities() includes newly stored entities with is_deleted=False and entries deleted by string id with is_deleted=True.
1 parent 573074e commit cb4ac17

3 files changed

Lines changed: 263 additions & 0 deletions

File tree

ravendb/documents/session/document_session.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,12 @@ def graph_query(self, object_type: type, query: str): # -> GraphDocumentQuery:
803803
def what_changed(self) -> Dict[str, List[DocumentsChanges]]:
804804
return self._session._what_changed()
805805

806+
def what_changed_for(self, entity: object) -> List[DocumentsChanges]:
807+
return self._session._what_changed_for(entity)
808+
809+
def get_tracked_entities(self) -> Dict[str, dict]:
810+
return self._session._get_tracked_entities()
811+
806812
def exists(self, key: str) -> bool:
807813
if key is None:
808814
raise ValueError("Key cannot be None")

ravendb/documents/session/document_session_operations/in_memory_document_session_operations.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,6 +1183,51 @@ def _what_changed(self) -> Dict[str, List[DocumentsChanges]]:
11831183

11841184
return changes
11851185

1186+
def _what_changed_for(self, entity: object) -> List[DocumentsChanges]:
1187+
if (doc_info := self._documents_by_entity.get(entity)) is None:
1188+
return []
1189+
if entity in self._deleted_entities:
1190+
return [
1191+
DocumentsChanges(
1192+
field_old_value="",
1193+
field_new_value="",
1194+
change=DocumentsChanges.ChangeType.DOCUMENT_DELETED,
1195+
)
1196+
]
1197+
_update_metadata_modifications(doc_info.metadata_instance, doc_info.metadata)
1198+
new_obj = self.entity_to_json.convert_entity_to_json(entity, doc_info)
1199+
changes: Dict[str, List[DocumentsChanges]] = {}
1200+
if not self._entity_changed(new_obj, doc_info, changes):
1201+
return []
1202+
return [
1203+
DocumentsChanges(
1204+
field_old_value=d["old_value"],
1205+
field_new_value=d["new_value"],
1206+
change=d["change"],
1207+
field_name=d["field_name"],
1208+
field_path=d["field_path"],
1209+
)
1210+
for d in changes.get(doc_info.key, [])
1211+
]
1212+
1213+
def _get_tracked_entities(self) -> Dict[str, dict]:
1214+
result = {}
1215+
for entity_result in self._documents_by_entity:
1216+
doc_info = entity_result.value
1217+
result[doc_info.key] = {
1218+
"id": doc_info.key,
1219+
"entity": entity_result.key,
1220+
"is_deleted": self.is_deleted(doc_info.key),
1221+
}
1222+
for key in self._known_missing_ids:
1223+
if key not in result:
1224+
result[key] = {
1225+
"id": key,
1226+
"entity": None,
1227+
"is_deleted": True,
1228+
}
1229+
return result
1230+
11861231
def __get_all_entities_changes(self, changes: Dict[str, List[DocumentsChanges]]) -> None:
11871232
for key, value in self._documents_by_id.items():
11881233
_update_metadata_modifications(value.metadata_instance, value.metadata)
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
"""C# ref: WhatChangedFor.cs, TrackEntity.cs, WhatChanged.cs"""
2+
3+
from ravendb.documents.session.misc import DocumentsChanges
4+
from ravendb.tests.test_base import TestBase
5+
6+
7+
class User:
8+
def __init__(self, name: str = "", age: int = 0):
9+
self.name = name
10+
self.age = age
11+
12+
13+
class Obj:
14+
def __init__(self, id: str = None, a: str = None, b: str = None):
15+
self.Id = id
16+
self.A = a
17+
self.B = b
18+
19+
20+
class Arr:
21+
def __init__(self, arr=None):
22+
self.arr = arr if arr is not None else []
23+
24+
25+
class TestRavenDBSessionAdvancedApi(TestBase):
26+
def setUp(self):
27+
super().setUp()
28+
29+
def test_what_changed_for_change_field(self):
30+
with self.store.open_session() as session:
31+
session.store(User(name="Alice"), "users/1")
32+
session.save_changes()
33+
34+
with self.store.open_session() as session:
35+
user = session.load("users/1", User)
36+
user.age = 5
37+
38+
result = session.advanced.what_changed_for(user)
39+
self.assertEqual(1, len(result))
40+
self.assertEqual(DocumentsChanges.ChangeType.FIELD_CHANGED, result[0].change)
41+
42+
def test_what_changed_for_new_field_before_save(self):
43+
"""What_Changed_For_New_Field — first block: before SaveChanges."""
44+
with self.store.open_session() as session:
45+
user = User(name="Alice")
46+
session.store(user, "users/1")
47+
48+
result = session.advanced.what_changed_for(user)
49+
self.assertEqual(1, len(result))
50+
self.assertEqual(DocumentsChanges.ChangeType.DOCUMENT_ADDED, result[0].change)
51+
52+
def test_what_changed_for_new_field(self):
53+
"""What_Changed_For_New_Field — second block: load as wider type."""
54+
with self.store.open_session() as session:
55+
session.store(User(name="Toli"), "users/1")
56+
session.save_changes()
57+
58+
with self.store.open_session() as session:
59+
user = session.load("users/1", User)
60+
user.email = "toli@example.com"
61+
62+
result = session.advanced.what_changed_for(user)
63+
self.assertEqual(1, len(result))
64+
self.assertEqual(DocumentsChanges.ChangeType.NEW_FIELD, result[0].change)
65+
66+
def test_what_changed_for_removed_field(self):
67+
with self.store.open_session() as session:
68+
session.store(User(name="Toli", age=5), "users/1")
69+
session.save_changes()
70+
71+
with self.store.open_session() as session:
72+
user = session.load("users/1", User)
73+
del user.age
74+
75+
result = session.advanced.what_changed_for(user)
76+
self.assertEqual(1, len(result))
77+
self.assertEqual(DocumentsChanges.ChangeType.REMOVED_FIELD, result[0].change)
78+
79+
def test_what_changed_for_delete_after_change_value(self):
80+
"""RavenDB-13501: field changes discarded after delete in same session."""
81+
with self.store.open_session() as session:
82+
session.store(Obj(id="DEL", a="A", b="A"), "DEL")
83+
session.save_changes()
84+
85+
with self.store.open_session() as session:
86+
o = session.load("DEL", Obj)
87+
o.A = "B"
88+
o.B = "C"
89+
session.delete(o)
90+
91+
result = session.advanced.what_changed_for(o)
92+
self.assertEqual(1, len(result))
93+
self.assertEqual(DocumentsChanges.ChangeType.DOCUMENT_DELETED, result[0].change)
94+
95+
def test_get_tracked_entities(self):
96+
with self.store.open_session() as session:
97+
user = User(name="Bob")
98+
session.store(user, "users/2")
99+
100+
tracked = session.advanced.get_tracked_entities()
101+
self.assertIn("users/2", tracked)
102+
self.assertIs(user, tracked["users/2"]["entity"])
103+
self.assertFalse(tracked["users/2"]["is_deleted"])
104+
105+
def test_get_tracked_entities_delete_by_id(self):
106+
with self.store.open_session() as session:
107+
session.store(User(name="Eve"), "users/3")
108+
session.save_changes()
109+
110+
with self.store.open_session() as session:
111+
session.load("users/3", User)
112+
session.delete("users/3")
113+
114+
tracked = session.advanced.get_tracked_entities()
115+
self.assertIn("users/3", tracked)
116+
self.assertTrue(tracked["users/3"]["is_deleted"])
117+
118+
def test_get_tracked_entities_delete_by_entity(self):
119+
with self.store.open_session() as session:
120+
session.store(User(name="Frank"), "users/4")
121+
session.save_changes()
122+
123+
with self.store.open_session() as session:
124+
user = session.load("users/4", User)
125+
session.delete(user)
126+
127+
tracked = session.advanced.get_tracked_entities()
128+
self.assertIn("users/4", tracked)
129+
self.assertTrue(tracked["users/4"]["is_deleted"])
130+
131+
def test_what_changed_delete_after_change_value(self):
132+
"""RavenDB-13501: what_changed() reports only DOCUMENT_DELETED after delete."""
133+
with self.store.open_session() as session:
134+
session.store(Obj(id="ABC", a="A", b="A"), "ABC")
135+
session.save_changes()
136+
137+
with self.store.open_session() as session:
138+
o = session.load("ABC", Obj)
139+
o.A = "B"
140+
o.B = "C"
141+
session.delete(o)
142+
143+
changes = session.advanced.what_changed()
144+
145+
self.assertIn("ABC", changes)
146+
self.assertEqual(1, len(changes["ABC"]))
147+
self.assertEqual(DocumentsChanges.ChangeType.DOCUMENT_DELETED, changes["ABC"][0].change)
148+
149+
150+
class TestWhatChangedForArrayChanges(TestBase):
151+
def setUp(self):
152+
super().setUp()
153+
154+
def test_what_changed_for_array_value_changed(self):
155+
"""["a",1,"b"] -> ["a",2,"c"] produces 2 ARRAY_VALUE_CHANGED entries."""
156+
with self.store.open_session() as session:
157+
session.store(Arr(arr=["a", 1, "b"]), "arr/1")
158+
session.save_changes()
159+
160+
with self.store.open_session() as session:
161+
arr = session.load("arr/1", Arr)
162+
arr.arr = ["a", 2, "c"]
163+
164+
changes = session.advanced.what_changed_for(arr)
165+
166+
self.assertEqual(2, len(changes))
167+
self.assertEqual(DocumentsChanges.ChangeType.ARRAY_VALUE_CHANGED, changes[0].change)
168+
self.assertEqual(1, changes[0].field_old_value)
169+
self.assertEqual(2, changes[0].field_new_value)
170+
self.assertEqual(DocumentsChanges.ChangeType.ARRAY_VALUE_CHANGED, changes[1].change)
171+
self.assertEqual("b", changes[1].field_old_value)
172+
self.assertEqual("c", changes[1].field_new_value)
173+
174+
def test_what_changed_for_array_value_added(self):
175+
"""["a",1,"b"] -> ["a",1,"b","c",2] produces 2 ARRAY_VALUE_ADDED entries."""
176+
with self.store.open_session() as session:
177+
session.store(Arr(arr=["a", 1, "b"]), "arr/1")
178+
session.save_changes()
179+
180+
with self.store.open_session() as session:
181+
arr = session.load("arr/1", Arr)
182+
arr.arr = ["a", 1, "b", "c", 2]
183+
184+
changes = session.advanced.what_changed_for(arr)
185+
186+
self.assertEqual(2, len(changes))
187+
self.assertEqual(DocumentsChanges.ChangeType.ARRAY_VALUE_ADDED, changes[0].change)
188+
self.assertIsNone(changes[0].field_old_value)
189+
self.assertEqual("c", changes[0].field_new_value)
190+
self.assertEqual(DocumentsChanges.ChangeType.ARRAY_VALUE_ADDED, changes[1].change)
191+
self.assertIsNone(changes[1].field_old_value)
192+
self.assertEqual(2, changes[1].field_new_value)
193+
194+
def test_what_changed_for_array_value_removed(self):
195+
"""["a",1,"b"] -> ["a"] produces 2 ARRAY_VALUE_REMOVED entries."""
196+
with self.store.open_session() as session:
197+
session.store(Arr(arr=["a", 1, "b"]), "arr/1")
198+
session.save_changes()
199+
200+
with self.store.open_session() as session:
201+
arr = session.load("arr/1", Arr)
202+
arr.arr = ["a"]
203+
204+
changes = session.advanced.what_changed_for(arr)
205+
206+
self.assertEqual(2, len(changes))
207+
self.assertEqual(DocumentsChanges.ChangeType.ARRAY_VALUE_REMOVED, changes[0].change)
208+
self.assertEqual(1, changes[0].field_old_value)
209+
self.assertIsNone(changes[0].field_new_value)
210+
self.assertEqual(DocumentsChanges.ChangeType.ARRAY_VALUE_REMOVED, changes[1].change)
211+
self.assertEqual("b", changes[1].field_old_value)
212+
self.assertIsNone(changes[1].field_new_value)

0 commit comments

Comments
 (0)