Skip to content

Commit f0b2928

Browse files
authored
Merge pull request #271 from redknightlois/RDBC-1021
RDBC-1021/1024/1025 Fix no-tracking includes guard, session refresh in-place, and lazy load None
2 parents 5975b1a + 109b620 commit f0b2928

8 files changed

Lines changed: 241 additions & 8 deletions

File tree

ravendb/documents/session/document_session.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,13 @@ def load(
316316
result = load_operation.get_documents(object_type)
317317
return result.popitem()[1] if len(result) == 1 else result if result else None
318318

319+
if self.no_tracking:
320+
raise InvalidOperationException(
321+
"Cannot register includes when no_tracking is enabled. "
322+
"Included documents are not tracked, so subsequent load operations for that data will still trigger additional server requests. "
323+
"To avoid confusion, include operations are disallowed when tracking is disabled on the session or query."
324+
)
325+
319326
include_builder = IncludeBuilder(self.conventions)
320327
includes(include_builder)
321328

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
@@ -1842,16 +1842,19 @@ def _refresh_internal(self, entity: object, cmd: RavenCommand, document_info: Do
18421842
if document_info.entity is not None and not self.no_tracking:
18431843
self.entity_to_json.remove_from_missing(document_info.entity)
18441844

1845-
document_info.entity = self.entity_to_json.convert_to_entity(
1845+
refreshed = self.entity_to_json.convert_to_entity(
18461846
type(entity), document_info.key, document, not self.no_tracking
18471847
)
18481848
document_info.document = document
18491849

1850+
# Update the original entity object in-place so the caller's reference
1851+
# immediately reflects the new server state (C# behaviour).
18501852
try:
1851-
entity = deepcopy(document_info.entity)
1852-
except Error as e:
1853-
raise RuntimeError(f"Unable to refresh entity: {e.args[0]}", e)
1853+
entity.__dict__.update(refreshed.__dict__)
1854+
except Exception as e:
1855+
raise RuntimeError(f"Unable to refresh entity: {e}") from e
18541856

1857+
document_info.entity = entity
18551858
document_info_by_id = self._documents_by_id.get(document_info.key)
18561859
if document_info_by_id is not None:
18571860
document_info_by_id.entity = entity

ravendb/documents/session/operations/lazy.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@ def include(self, path: str) -> LazyMultiLoaderWithInclude:
191191
def load(
192192
self, ids: Union[List[str], str], object_type: Optional[Type[_T]] = None, on_eval: Callable = None
193193
) -> Optional[Lazy[Union[Dict[str, object], object]]]:
194+
if ids is None:
195+
return Lazy(lambda: None)
194196
if not ids:
195197
return None
196198

ravendb/documents/session/query.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
SuggestionBuilder,
5353
)
5454
from ravendb.documents.queries.utils import QueryFieldUtil
55+
from ravendb.exceptions.exceptions import InvalidOperationException
5556
from ravendb.documents.session.event_args import BeforeQueryEventArgs
5657
from ravendb.documents.session.loaders.include import IncludeBuilderBase, QueryIncludeBuilder
5758
from ravendb.documents.session.misc import MethodCall, CmpXchg, OrderingType, DocumentQueryCustomization
@@ -368,6 +369,12 @@ def __action():
368369
return MoreLikeThisScope(token, self.__add_query_parameter, __action)
369370

370371
def _include(self, path_or_include_builder: Union[str, IncludeBuilderBase]) -> None:
372+
if self._the_session is not None and self._the_session.no_tracking:
373+
raise InvalidOperationException(
374+
"Cannot register includes when no_tracking is enabled. "
375+
"Included documents are not tracked, so subsequent load operations for that data will still trigger additional server requests. "
376+
"To avoid confusion, include operations are disallowed when tracking is disabled on the session or query."
377+
)
371378
if isinstance(path_or_include_builder, str):
372379
self._document_includes.add(path_or_include_builder)
373380
elif isinstance(path_or_include_builder, IncludeBuilderBase):

ravendb/tests/jvm_migrated_tests/issues_tests/test_ravenDB_11217.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def test_session_wide_no_tracking_should_work(self):
3737
with self.store.open_session(session_options=no_tracking_options) as session:
3838
self.assertEqual(0, session.advanced.number_of_requests)
3939

40-
product1 = session.load("products/1-A", Product, lambda b: b.include_documents("supplier"))
40+
product1 = session.load("products/1-A", Product)
4141

4242
self.assertEqual(1, session.advanced.number_of_requests)
4343

@@ -52,7 +52,7 @@ def test_session_wide_no_tracking_should_work(self):
5252
self.assertEqual(2, session.advanced.number_of_requests)
5353
self.assertFalse(session.advanced.is_loaded(supplier.Id))
5454

55-
product2 = session.load("products/1-A", Product, lambda b: b.include_documents(("supplier")))
55+
product2 = session.load("products/1-A", Product)
5656
self.assertNotEqual(product2, product1)
5757

5858
with self.store.open_session(session_options=no_tracking_options) as session:
@@ -79,7 +79,7 @@ def test_session_wide_no_tracking_should_work(self):
7979

8080
with self.store.open_session(session_options=no_tracking_options) as session:
8181
self.assertEqual(0, session.advanced.number_of_requests)
82-
products = list(session.query(object_type=Product).include("supplier"))
82+
products = list(session.query(object_type=Product))
8383

8484
self.assertEqual(1, session.advanced.number_of_requests)
8585
self.assertEqual(1, len(products))
@@ -95,7 +95,7 @@ def test_session_wide_no_tracking_should_work(self):
9595
self.assertEqual(2, session.advanced.number_of_requests)
9696
self.assertFalse(session.advanced.is_loaded(supplier.Id))
9797

98-
products = list(session.query(object_type=Product).include("supplier"))
98+
products = list(session.query(object_type=Product))
9999
self.assertEqual(1, len(products))
100100

101101
product2 = products[0]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""
2+
Lazy load: lazily.load(None) returns a Lazy that resolves to None.
3+
4+
C# reference: SlowTests/Issues/RavenDB_21859.cs
5+
Load_And_Lazy_Load_Should_Return_Null_When_Id_Is_Null
6+
"""
7+
8+
from ravendb.tests.test_base import TestBase
9+
10+
11+
class User:
12+
def __init__(self, name: str = None):
13+
self.name = name
14+
15+
16+
class TestRavenDB21859(TestBase):
17+
def setUp(self):
18+
super().setUp()
19+
20+
def test_load_null_id_returns_none(self):
21+
with self.store.open_session() as session:
22+
result = session.load(None, User)
23+
self.assertIsNone(result)
24+
25+
def test_lazily_load_null_id_returns_none(self):
26+
with self.store.open_session() as session:
27+
lazy = session.advanced.lazily.load(None, User)
28+
session.advanced.eagerly.execute_all_pending_lazy_operations()
29+
self.assertIsNone(lazy.value)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""
2+
No-tracking session: using includes raises when session.no_tracking is True.
3+
4+
C# reference: SlowTests/Issues/RavenDB_21339.cs
5+
Using_Includes_In_Non_Tracking_Session_Should_Throw
6+
"""
7+
8+
from ravendb.documents.session.misc import SessionOptions
9+
from ravendb.tests.test_base import TestBase
10+
11+
12+
class Manager:
13+
def __init__(self, name: str = None):
14+
self.name = name
15+
16+
17+
class Employee:
18+
def __init__(self, name: str = None, manager_id: str = None):
19+
self.name = name
20+
self.manager_id = manager_id
21+
22+
23+
class TestRavenDB21339(TestBase):
24+
def setUp(self):
25+
super().setUp()
26+
27+
def _populate(self):
28+
with self.store.open_session() as session:
29+
session.store(Manager(name="Boss"), "managers/1")
30+
session.store(Employee(name="Alice", manager_id="managers/1"), "employees/1")
31+
session.save_changes()
32+
33+
def test_load_with_includes_in_no_tracking_session_should_throw(self):
34+
"""
35+
session.load() with an include builder in a no_tracking session must
36+
raise an exception, not silently ignore the includes."""
37+
self._populate()
38+
39+
with self.store.open_session(session_options=SessionOptions(no_tracking=True)) as session:
40+
with self.assertRaises(Exception) as ctx:
41+
session.load(
42+
"employees/1",
43+
Employee,
44+
includes=lambda b: b.include_documents("manager_id"),
45+
)
46+
self.assertIn("Cannot register includes when no_tracking is enabled", str(ctx.exception))
47+
48+
def test_query_with_include_in_no_tracking_session_should_throw(self):
49+
"""
50+
query.include() in a no_tracking session must raise an exception,
51+
not silently add the include to an unused tracking cache."""
52+
self._populate()
53+
54+
with self.store.open_session(session_options=SessionOptions(no_tracking=True)) as session:
55+
with self.assertRaises(Exception) as ctx:
56+
list(session.query(object_type=Employee).include("manager_id").wait_for_non_stale_results())
57+
self.assertIn("Cannot register includes when no_tracking is enabled", str(ctx.exception))
58+
59+
def test_document_query_with_include_in_no_tracking_session_should_throw(self):
60+
"""
61+
C# spec: session.Advanced.DocumentQuery<Product>().Include(x => x.Supplier).ToList()
62+
in a no_tracking session must raise InvalidOperationException.
63+
"""
64+
self._populate()
65+
66+
with self.store.open_session(session_options=SessionOptions(no_tracking=True)) as session:
67+
with self.assertRaises(Exception) as ctx:
68+
list(session.advanced.document_query(object_type=Employee).include("manager_id"))
69+
self.assertIn("Cannot register includes when no_tracking is enabled", str(ctx.exception))
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)