Skip to content

Commit 79c6403

Browse files
committed
RDBC-1024: session.advanced.refresh(entity) now updates the entity in-place
_refresh_internal() was converting a fresh entity and deep-copying it, leaving the caller's original reference stale. Fix: update entity.__dict__ from the freshly converted object so the caller sees the new server state without needing to use the return value. Regression test: test_session_refresh.py verifies that the original entity reference reflects the updated server state after refresh() is called, that the return value is tracked, and that the change vector is updated.
1 parent e0c6e21 commit 79c6403

2 files changed

Lines changed: 123 additions & 4 deletions

File tree

ravendb/documents/session/document_session_operations/in_memory_document_session_operations.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1789,16 +1789,19 @@ def _refresh_internal(self, entity: object, cmd: RavenCommand, document_info: Do
17891789
if document_info.entity is not None and not self.no_tracking:
17901790
self.entity_to_json.remove_from_missing(document_info.entity)
17911791

1792-
document_info.entity = self.entity_to_json.convert_to_entity(
1792+
refreshed = self.entity_to_json.convert_to_entity(
17931793
type(entity), document_info.key, document, not self.no_tracking
17941794
)
17951795
document_info.document = document
17961796

1797+
# Update the original entity object in-place so the caller's reference
1798+
# immediately reflects the new server state (C# behaviour).
17971799
try:
1798-
entity = deepcopy(document_info.entity)
1799-
except Error as e:
1800-
raise RuntimeError(f"Unable to refresh entity: {e.args[0]}", e)
1800+
entity.__dict__.update(refreshed.__dict__)
1801+
except Exception as e:
1802+
raise RuntimeError(f"Unable to refresh entity: {e}") from e
18011803

1804+
document_info.entity = entity
18021805
document_info_by_id = self._documents_by_id.get(document_info.key)
18031806
if document_info_by_id is not None:
18041807
document_info_by_id.entity = entity
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""
2+
Session refresh: advanced.refresh(entity) re-fetches from the server and updates
3+
the entity object in-place; the refreshed entity stays tracked in the session.
4+
5+
C# reference: FastTests/Client/Store.cs
6+
Refresh_stored_document
7+
"""
8+
9+
from ravendb.tests.test_base import TestBase
10+
11+
12+
class User:
13+
def __init__(self, name: str = "", age: int = 0):
14+
self.name = name
15+
self.age = age
16+
17+
18+
class TestRavenDBRefreshStoredDocument(TestBase):
19+
def setUp(self):
20+
super().setUp()
21+
22+
def test_refresh_updates_entity_in_place(self):
23+
"""
24+
C# spec: after Advanced.Refresh(user), the SAME user object must
25+
reflect the server state. No need to use the return value.
26+
27+
must use the return value — but that returned entity is not tracked
28+
by the session (see test_refresh_returned_entity_is_tracked below)."""
29+
with self.store.open_session() as s:
30+
s.store(User(name="RVN", age=1), "users/1")
31+
s.save_changes()
32+
33+
with self.store.open_session() as outer:
34+
user = outer.load("users/1", User)
35+
self.assertEqual(1, user.age, "precondition: age=1 after load")
36+
37+
# External session updates age to 10 (mirrors C# inner session)
38+
with self.store.open_session() as inner:
39+
u2 = inner.load("users/1", User)
40+
u2.age = 10
41+
inner.save_changes()
42+
43+
# C# spec: after Refresh(user), user.Age == 10
44+
outer.advanced.refresh(user)
45+
46+
self.assertEqual(
47+
10,
48+
user.age,
49+
msg=f"refresh() did not update the entity in-place. user.age is still {user.age} (expected 10).",
50+
)
51+
52+
def test_refresh_returned_entity_is_tracked(self):
53+
"""
54+
refresh() returns the same (mutated-in-place) entity object.
55+
That returned value must be session-tracked so that metadata
56+
APIs such as get_change_vector_for() work on it."""
57+
with self.store.open_session() as s:
58+
s.store(User(name="RVN", age=1), "users/1")
59+
s.save_changes()
60+
61+
with self.store.open_session() as outer:
62+
user = outer.load("users/1", User)
63+
64+
with self.store.open_session() as inner:
65+
u2 = inner.load("users/1", User)
66+
u2.age = 10
67+
inner.save_changes()
68+
69+
returned = outer.advanced.refresh(user)
70+
71+
# The returned entity should be the refreshed copy.
72+
self.assertEqual(
73+
10,
74+
returned.age,
75+
msg="Return value of refresh() should have the updated age.",
76+
)
77+
78+
# The returned entity must be session-tracked so metadata APIs work.
79+
cv = outer.advanced.get_change_vector_for(returned)
80+
self.assertIsNotNone(cv, "Change vector should be non-None for refreshed entity")
81+
82+
def test_refresh_updates_change_vector_for_original_entity(self):
83+
"""
84+
C# spec (Refresh_stored_document): after Refresh(user),
85+
Advanced.GetChangeVectorFor(user) must return the NEW change vector
86+
(not the one recorded at load time), and GetLastModifiedFor(user)
87+
must return the new timestamp."""
88+
with self.store.open_session() as s:
89+
s.store(User(name="RVN", age=1), "users/1")
90+
s.save_changes()
91+
92+
with self.store.open_session() as outer:
93+
user = outer.load("users/1", User)
94+
cv_before = outer.advanced.get_change_vector_for(user)
95+
lm_before = outer.advanced.get_last_modified_for(user)
96+
97+
with self.store.open_session() as inner:
98+
u2 = inner.load("users/1", User)
99+
u2.age = 10
100+
inner.save_changes()
101+
102+
outer.advanced.refresh(user)
103+
104+
cv_after = outer.advanced.get_change_vector_for(user)
105+
lm_after = outer.advanced.get_last_modified_for(user)
106+
107+
self.assertNotEqual(
108+
cv_before,
109+
cv_after,
110+
msg="GetChangeVectorFor(user) must return a new change vector after Refresh()",
111+
)
112+
self.assertNotEqual(
113+
lm_before,
114+
lm_after,
115+
msg="GetLastModifiedFor(user) must return a new timestamp after Refresh()",
116+
)

0 commit comments

Comments
 (0)