From fc203cd843dd585b703099444e4db876c07683f8 Mon Sep 17 00:00:00 2001 From: Federico Andres Lois Date: Wed, 25 Feb 2026 14:10:25 -0300 Subject: [PATCH 1/3] RDBC-1018: break metadata dict alias in DocumentInfo.get_new_document_info document_info.metadata was the same Python object as document['@metadata'], so _update_metadata_modifications() was mutating the comparison baseline and making metadata-only changes invisible to what_changed() / has_changed(). Store dict(metadata) to break the alias. Regression test: test_change_tracking_metadata.py verifies that modifying or deleting metadata on a stored entity is detected by what_changed(), has_changed(), and has_changes(), and that changes are persisted by save_changes(). --- ravendb/documents/session/document_info.py | 3 +- .../test_change_tracking_metadata.py | 242 ++++++++++++++++++ 2 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 ravendb/tests/session_tests/test_change_tracking_metadata.py diff --git a/ravendb/documents/session/document_info.py b/ravendb/documents/session/document_info.py index bc8de6a1..7036440d 100644 --- a/ravendb/documents/session/document_info.py +++ b/ravendb/documents/session/document_info.py @@ -44,4 +44,5 @@ def get_new_document_info(cls, document: Dict) -> DocumentInfo: if not change_vector or not isinstance(change_vector, str): raise ValueError(f"Document {key} must have a Change Vector") - return cls(key=key, document=document, metadata=metadata, entity=None, change_vector=change_vector) + # Shallow-copy metadata so mutations on this DocumentInfo don't alias the original document dict + return cls(key=key, document=document, metadata=dict(metadata), entity=None, change_vector=change_vector) diff --git a/ravendb/tests/session_tests/test_change_tracking_metadata.py b/ravendb/tests/session_tests/test_change_tracking_metadata.py new file mode 100644 index 00000000..7ddb1fe7 --- /dev/null +++ b/ravendb/tests/session_tests/test_change_tracking_metadata.py @@ -0,0 +1,242 @@ +"""C# ref: FastTests/Client/WhatChanged.cs — WhatChanged_should_be_idempotent_operation (RavenDB-9150)""" + +from ravendb.tests.test_base import TestBase + + +class User: + def __init__(self, name: str = "", age: int = 0): + self.name = name + self.age = age + + +class TestRavenDBWhatChangedIdempotent(TestBase): + def setUp(self): + super().setUp() + + def test_what_changed_should_be_idempotent_operation(self): + """No modifications — two calls return the same empty result.""" + with self.store.open_session() as session: + session.store(User(name="Alice", age=30), "users/1") + session.save_changes() + + with self.store.open_session() as session: + session.load("users/1", User) + + result_1 = session.advanced.what_changed() + result_2 = session.advanced.what_changed() + + self.assertEqual(result_1, result_2) + + def test_what_changed_should_be_idempotent_operation_with_changes(self): + """With modifications — two calls return the same non-empty result.""" + with self.store.open_session() as session: + session.store(User(name="user1"), "users/2") + session.store(User(name="user2", age=1), "users/3") + session.save_changes() + + with self.store.open_session() as session: + user1 = session.load("users/2", User) + user2 = session.load("users/3", User) + + user1.age = 10 + session.delete(user2) + + result_1 = session.advanced.what_changed() + result_2 = session.advanced.what_changed() + + self.assertEqual(2, len(result_1)) + self.assertEqual(len(result_1), len(result_2)) + + def test_what_changed_detects_metadata_modification(self): + with self.store.open_session() as session: + session.store(User(name="Bob"), "users/4") + session.save_changes() + + with self.store.open_session() as session: + user = session.load("users/4", User) + meta = session.advanced.get_metadata_for(user) + meta["@custom-tag"] = "v1" + + changes = session.advanced.what_changed() + self.assertIn("users/4", changes) + + def test_what_changed_and_save_changes_agree_on_metadata_write(self): + with self.store.open_session() as session: + session.store(User(name="Carol"), "users/5") + session.save_changes() + + with self.store.open_session() as session: + user = session.load("users/5", User) + meta = session.advanced.get_metadata_for(user) + meta["@custom-tag"] = "v2" + + pending = session.advanced.what_changed() + self.assertIn("users/5", pending) + + session.save_changes() + + with self.store.open_session() as session: + reloaded_meta = session.advanced.get_metadata_for(session.load("users/5", User)) + self.assertEqual("v2", reloaded_meta.get("@custom-tag")) + + +class TestRavenDBWhatChangedMetadataOps(TestBase): + def setUp(self): + super().setUp() + + def test_has_changed_returns_true_for_metadata_only_modification(self): + with self.store.open_session() as session: + session.store(User(name="Alice"), "users/1") + session.save_changes() + + with self.store.open_session() as session: + user = session.load("users/1", User) + meta = session.advanced.get_metadata_for(user) + meta["@custom"] = "v1" + + self.assertTrue(session.advanced.has_changed(user)) + + def test_has_changed_returns_false_when_no_modification(self): + with self.store.open_session() as session: + session.store(User(name="Bob"), "users/2") + session.save_changes() + + with self.store.open_session() as session: + session.load("users/2", User) + user = session.load("users/2", User) + + self.assertFalse(session.advanced.has_changed(user)) + + def test_has_changes_returns_true_for_metadata_only_modification(self): + with self.store.open_session() as session: + session.store(User(name="Carol"), "users/3") + session.save_changes() + + with self.store.open_session() as session: + user = session.load("users/3", User) + meta = session.advanced.get_metadata_for(user) + meta["@custom"] = "v1" + + self.assertTrue(session.advanced.has_changes()) + + def test_has_changes_returns_false_when_no_modification(self): + with self.store.open_session() as session: + session.store(User(name="Dave"), "users/4") + session.save_changes() + + with self.store.open_session() as session: + session.load("users/4", User) + self.assertFalse(session.advanced.has_changes()) + + def test_what_changed_detects_metadata_key_deletion(self): + with self.store.open_session() as session: + session.store(User(name="Eve"), "users/5") + session.save_changes() + + with self.store.open_session() as session: + user = session.load("users/5", User) + meta = session.advanced.get_metadata_for(user) + meta["@custom"] = "to-be-deleted" + session.save_changes() + + with self.store.open_session() as session: + user = session.load("users/5", User) + meta = session.advanced.get_metadata_for(user) + self.assertEqual("to-be-deleted", meta.get("@custom")) + del meta["@custom"] + + changes = session.advanced.what_changed() + self.assertIn("users/5", changes) + + def test_metadata_deletion_is_detected_and_persisted(self): + with self.store.open_session() as session: + session.store(User(name="Frank"), "users/6") + session.save_changes() + + with self.store.open_session() as session: + user = session.load("users/6", User) + meta = session.advanced.get_metadata_for(user) + meta["@tag"] = "remove-me" + session.save_changes() + + with self.store.open_session() as session: + user = session.load("users/6", User) + meta = session.advanced.get_metadata_for(user) + del meta["@tag"] + + self.assertIn("users/6", session.advanced.what_changed()) + session.save_changes() + + with self.store.open_session() as session: + meta = session.advanced.get_metadata_for(session.load("users/6", User)) + self.assertNotIn("@tag", meta) + + +class Doc: + def __init__(self, name: str = ""): + self.name = name + + +class TestRavenDBWhatChangedMetadataMutations(TestBase): + def setUp(self): + super().setUp() + with self.store.open_session() as session: + d = Doc() + session.store(d, "d/1") + meta = session.advanced.get_metadata_for(d) + meta["Test-A"] = ["a", "a", "a"] + meta["Test-C"] = ["c", "c", "c"] + session.save_changes() + + def test_metadata_array_value_change_detected(self): + """meta["Test-A"] = ["b","a","c"] -> 2 ARRAY_VALUE_CHANGED entries.""" + with self.store.open_session() as session: + d = session.load("d/1", Doc) + meta = session.advanced.get_metadata_for(d) + meta["Test-A"] = ["b", "a", "c"] + + changes = session.advanced.what_changed() + + self.assertIn("d/1", changes) + self.assertEqual(2, len(changes["d/1"])) + + def test_metadata_key_removal_detected(self): + """meta.Remove("Test-A") -> 1 REMOVED_FIELD entry.""" + with self.store.open_session() as session: + d = session.load("d/1", Doc) + meta = session.advanced.get_metadata_for(d) + meta.pop("Test-A") + + changes = session.advanced.what_changed() + + self.assertIn("d/1", changes) + change_types = [str(c["change"]) for c in changes["d/1"]] + self.assertIn("removed_field", change_types) + + def test_metadata_remove_two_add_two_detected(self): + """Remove Test-A, Test-C; add Test-B, Test-D -> 4 entries.""" + with self.store.open_session() as session: + d = session.load("d/1", Doc) + meta = session.advanced.get_metadata_for(d) + meta.pop("Test-A") + meta.pop("Test-C") + meta["Test-B"] = ["b", "b", "b"] + meta["Test-D"] = ["d", "d", "d"] + + changes = session.advanced.what_changed() + + self.assertIn("d/1", changes) + self.assertEqual(4, len(changes["d/1"])) + + def test_metadata_remove_one_add_one_detected(self): + """Remove Test-A; add Test-B -> 2 entries.""" + with self.store.open_session() as session: + d = session.load("d/1", Doc) + meta = session.advanced.get_metadata_for(d) + meta.pop("Test-A") + meta["Test-B"] = ["b", "b", "b"] + + changes = session.advanced.what_changed() + + self.assertIn("d/1", changes) + self.assertEqual(2, len(changes["d/1"])) From 573074e9fe8dff8d3b9c7f984d2fd6d02ed10dde Mon Sep 17 00:00:00 2001 From: Federico Andres Lois Date: Wed, 25 Feb 2026 14:10:33 -0300 Subject: [PATCH 2/3] RDBC-1019: fix type-coercion in value comparison (arrays and scalar fields) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit compare_json_array and compare_json both used equality-only comparisons for primitive values, making 1 and '1' (or True and 1) compare equal — silent data loss. Fix: use compare_values(old, new) = old==new AND type(old)==type(new) for all primitive comparisons, both in array elements and top-level scalar fields. The array path had str(old) == str(new). The scalar path had `new_prop == old_prop or compare_values(...)` where compare_values was unreachable dead code (it can only be True when the first operand is already True). Both are replaced with `if JsonOperation.compare_values(old_prop, new_prop): continue`. Regression tests verify that array and scalar fields changing type (int→str, bool→int) are detected as changes and persisted by save_changes(). --- ravendb/json/json_operation.py | 7 +- .../test_change_tracking_type_coercion.py | 87 +++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 ravendb/tests/session_tests/test_change_tracking_type_coercion.py diff --git a/ravendb/json/json_operation.py b/ravendb/json/json_operation.py index 7d33e5b4..c3224400 100644 --- a/ravendb/json/json_operation.py +++ b/ravendb/json/json_operation.py @@ -82,7 +82,7 @@ def compare_json( old_prop = original_json[prop] if isinstance(new_prop, (int, float, bool, str)): - if new_prop == old_prop or JsonOperation.compare_values(old_prop, new_prop): + if JsonOperation.compare_values(old_prop, new_prop): continue if changes is None: return True @@ -193,7 +193,10 @@ def compare_json_array(field_path: str, key: str, old_collection, new_collection DocumentsChanges.ChangeType.ARRAY_VALUE_CHANGED, ) elif isinstance(new_collection_item, (int, float, bool, str)): - if not str(old_collection_item) == str(new_collection_item): + if ( + type(old_collection_item) is not type(new_collection_item) + or old_collection_item != new_collection_item + ): if changes is not None: JsonOperation.new_change( JsonOperation.add_index_field_path(field_path, position), diff --git a/ravendb/tests/session_tests/test_change_tracking_type_coercion.py b/ravendb/tests/session_tests/test_change_tracking_type_coercion.py new file mode 100644 index 00000000..98e8e424 --- /dev/null +++ b/ravendb/tests/session_tests/test_change_tracking_type_coercion.py @@ -0,0 +1,87 @@ +"""C# ref: FastTests/Client/WhatChanged.cs — RavenDB_8169""" + +from ravendb.tests.test_base import TestBase + + +class Doc: + def __init__(self, numbers=None): + self.numbers = numbers if numbers is not None else [] + + +class TestRavenDBWhatChangedTypeCoercion(TestBase): + def setUp(self): + super().setUp() + + def test_ravendb_8169(self): + """int->str array change: [1,2,3] -> ["1","2","3"] produces 3 ARRAY_VALUE_CHANGED.""" + with self.store.open_session() as session: + session.store(Doc(numbers=[1, 2, 3]), "docs/1") + session.save_changes() + + with self.store.open_session() as session: + doc = session.load("docs/1", Doc) + doc.numbers = ["1", "2", "3"] + + changes = session.advanced.what_changed() + + self.assertIn("docs/1", changes) + changed = [c for c in changes["docs/1"] if str(c["change"]) == "array_value_changed"] + self.assertEqual(3, len(changed)) + + def test_ravendb_8169_persisted(self): + """int->str array change is persisted after save_changes().""" + with self.store.open_session() as session: + session.store(Doc(numbers=[1, 2, 3]), "docs/2") + session.save_changes() + + with self.store.open_session() as session: + doc = session.load("docs/2", Doc) + doc.numbers = ["1", "2", "3"] + session.save_changes() + + with self.store.open_session() as session: + reloaded = session.load("docs/2", Doc) + self.assertEqual(["1", "2", "3"], reloaded.numbers) + + +class ScalarDoc: + def __init__(self, value=None, flag=None): + self.value = value + self.flag = flag + + +class TestRavenDBWhatChangedScalarTypeCoercion(TestBase): + def setUp(self): + super().setUp() + + def test_int_to_string_scalar_field_change_is_detected(self): + """int 1 -> str "1" on a scalar field produces FIELD_CHANGED.""" + with self.store.open_session() as session: + session.store(ScalarDoc(value=1), "scalar/1") + session.save_changes() + + with self.store.open_session() as session: + doc = session.load("scalar/1", ScalarDoc) + doc.value = "1" + + changes = session.advanced.what_changed() + + self.assertIn("scalar/1", changes) + field_changes = [c for c in changes["scalar/1"] if str(c["change"]) == "field_changed"] + self.assertEqual(1, len(field_changes)) + + def test_bool_to_int_scalar_field_change_is_detected(self): + """bool True -> int 1 produces FIELD_CHANGED (Python: True == 1 is True).""" + with self.store.open_session() as session: + session.store(ScalarDoc(flag=True), "scalar/2") + session.save_changes() + + with self.store.open_session() as session: + doc = session.load("scalar/2", ScalarDoc) + doc.flag = 1 + + changes = session.advanced.what_changed() + + self.assertIn("scalar/2", changes) + field_changes = [c for c in changes["scalar/2"] if str(c["change"]) == "field_changed"] + self.assertEqual(1, len(field_changes)) From cb4ac17acbec12a3606e8bb93a272b5fcd9c24df Mon Sep 17 00:00:00 2001 From: Federico Andres Lois Date: Wed, 25 Feb 2026 14:10:53 -0300 Subject: [PATCH 3/3] 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. --- ravendb/documents/session/document_session.py | 6 + .../in_memory_document_session_operations.py | 45 ++++ .../test_session_advanced_api.py | 212 ++++++++++++++++++ 3 files changed, 263 insertions(+) create mode 100644 ravendb/tests/session_tests/test_session_advanced_api.py diff --git a/ravendb/documents/session/document_session.py b/ravendb/documents/session/document_session.py index 327da2ea..77a3905b 100644 --- a/ravendb/documents/session/document_session.py +++ b/ravendb/documents/session/document_session.py @@ -803,6 +803,12 @@ def graph_query(self, object_type: type, query: str): # -> GraphDocumentQuery: def what_changed(self) -> Dict[str, List[DocumentsChanges]]: return self._session._what_changed() + def what_changed_for(self, entity: object) -> List[DocumentsChanges]: + return self._session._what_changed_for(entity) + + def get_tracked_entities(self) -> Dict[str, dict]: + return self._session._get_tracked_entities() + def exists(self, key: str) -> bool: if key is None: raise ValueError("Key cannot be None") diff --git a/ravendb/documents/session/document_session_operations/in_memory_document_session_operations.py b/ravendb/documents/session/document_session_operations/in_memory_document_session_operations.py index c1802819..3c2ea4e6 100644 --- a/ravendb/documents/session/document_session_operations/in_memory_document_session_operations.py +++ b/ravendb/documents/session/document_session_operations/in_memory_document_session_operations.py @@ -1183,6 +1183,51 @@ def _what_changed(self) -> Dict[str, List[DocumentsChanges]]: return changes + def _what_changed_for(self, entity: object) -> List[DocumentsChanges]: + if (doc_info := self._documents_by_entity.get(entity)) is None: + return [] + if entity in self._deleted_entities: + return [ + DocumentsChanges( + field_old_value="", + field_new_value="", + change=DocumentsChanges.ChangeType.DOCUMENT_DELETED, + ) + ] + _update_metadata_modifications(doc_info.metadata_instance, doc_info.metadata) + new_obj = self.entity_to_json.convert_entity_to_json(entity, doc_info) + changes: Dict[str, List[DocumentsChanges]] = {} + if not self._entity_changed(new_obj, doc_info, changes): + return [] + return [ + DocumentsChanges( + field_old_value=d["old_value"], + field_new_value=d["new_value"], + change=d["change"], + field_name=d["field_name"], + field_path=d["field_path"], + ) + for d in changes.get(doc_info.key, []) + ] + + def _get_tracked_entities(self) -> Dict[str, dict]: + result = {} + for entity_result in self._documents_by_entity: + doc_info = entity_result.value + result[doc_info.key] = { + "id": doc_info.key, + "entity": entity_result.key, + "is_deleted": self.is_deleted(doc_info.key), + } + for key in self._known_missing_ids: + if key not in result: + result[key] = { + "id": key, + "entity": None, + "is_deleted": True, + } + return result + def __get_all_entities_changes(self, changes: Dict[str, List[DocumentsChanges]]) -> None: for key, value in self._documents_by_id.items(): _update_metadata_modifications(value.metadata_instance, value.metadata) diff --git a/ravendb/tests/session_tests/test_session_advanced_api.py b/ravendb/tests/session_tests/test_session_advanced_api.py new file mode 100644 index 00000000..8b9ef791 --- /dev/null +++ b/ravendb/tests/session_tests/test_session_advanced_api.py @@ -0,0 +1,212 @@ +"""C# ref: WhatChangedFor.cs, TrackEntity.cs, WhatChanged.cs""" + +from ravendb.documents.session.misc import DocumentsChanges +from ravendb.tests.test_base import TestBase + + +class User: + def __init__(self, name: str = "", age: int = 0): + self.name = name + self.age = age + + +class Obj: + def __init__(self, id: str = None, a: str = None, b: str = None): + self.Id = id + self.A = a + self.B = b + + +class Arr: + def __init__(self, arr=None): + self.arr = arr if arr is not None else [] + + +class TestRavenDBSessionAdvancedApi(TestBase): + def setUp(self): + super().setUp() + + def test_what_changed_for_change_field(self): + with self.store.open_session() as session: + session.store(User(name="Alice"), "users/1") + session.save_changes() + + with self.store.open_session() as session: + user = session.load("users/1", User) + user.age = 5 + + result = session.advanced.what_changed_for(user) + self.assertEqual(1, len(result)) + self.assertEqual(DocumentsChanges.ChangeType.FIELD_CHANGED, result[0].change) + + def test_what_changed_for_new_field_before_save(self): + """What_Changed_For_New_Field — first block: before SaveChanges.""" + with self.store.open_session() as session: + user = User(name="Alice") + session.store(user, "users/1") + + result = session.advanced.what_changed_for(user) + self.assertEqual(1, len(result)) + self.assertEqual(DocumentsChanges.ChangeType.DOCUMENT_ADDED, result[0].change) + + def test_what_changed_for_new_field(self): + """What_Changed_For_New_Field — second block: load as wider type.""" + with self.store.open_session() as session: + session.store(User(name="Toli"), "users/1") + session.save_changes() + + with self.store.open_session() as session: + user = session.load("users/1", User) + user.email = "toli@example.com" + + result = session.advanced.what_changed_for(user) + self.assertEqual(1, len(result)) + self.assertEqual(DocumentsChanges.ChangeType.NEW_FIELD, result[0].change) + + def test_what_changed_for_removed_field(self): + with self.store.open_session() as session: + session.store(User(name="Toli", age=5), "users/1") + session.save_changes() + + with self.store.open_session() as session: + user = session.load("users/1", User) + del user.age + + result = session.advanced.what_changed_for(user) + self.assertEqual(1, len(result)) + self.assertEqual(DocumentsChanges.ChangeType.REMOVED_FIELD, result[0].change) + + def test_what_changed_for_delete_after_change_value(self): + """RavenDB-13501: field changes discarded after delete in same session.""" + with self.store.open_session() as session: + session.store(Obj(id="DEL", a="A", b="A"), "DEL") + session.save_changes() + + with self.store.open_session() as session: + o = session.load("DEL", Obj) + o.A = "B" + o.B = "C" + session.delete(o) + + result = session.advanced.what_changed_for(o) + self.assertEqual(1, len(result)) + self.assertEqual(DocumentsChanges.ChangeType.DOCUMENT_DELETED, result[0].change) + + def test_get_tracked_entities(self): + with self.store.open_session() as session: + user = User(name="Bob") + session.store(user, "users/2") + + tracked = session.advanced.get_tracked_entities() + self.assertIn("users/2", tracked) + self.assertIs(user, tracked["users/2"]["entity"]) + self.assertFalse(tracked["users/2"]["is_deleted"]) + + def test_get_tracked_entities_delete_by_id(self): + with self.store.open_session() as session: + session.store(User(name="Eve"), "users/3") + session.save_changes() + + with self.store.open_session() as session: + session.load("users/3", User) + session.delete("users/3") + + tracked = session.advanced.get_tracked_entities() + self.assertIn("users/3", tracked) + self.assertTrue(tracked["users/3"]["is_deleted"]) + + def test_get_tracked_entities_delete_by_entity(self): + with self.store.open_session() as session: + session.store(User(name="Frank"), "users/4") + session.save_changes() + + with self.store.open_session() as session: + user = session.load("users/4", User) + session.delete(user) + + tracked = session.advanced.get_tracked_entities() + self.assertIn("users/4", tracked) + self.assertTrue(tracked["users/4"]["is_deleted"]) + + def test_what_changed_delete_after_change_value(self): + """RavenDB-13501: what_changed() reports only DOCUMENT_DELETED after delete.""" + with self.store.open_session() as session: + session.store(Obj(id="ABC", a="A", b="A"), "ABC") + session.save_changes() + + with self.store.open_session() as session: + o = session.load("ABC", Obj) + o.A = "B" + o.B = "C" + session.delete(o) + + changes = session.advanced.what_changed() + + self.assertIn("ABC", changes) + self.assertEqual(1, len(changes["ABC"])) + self.assertEqual(DocumentsChanges.ChangeType.DOCUMENT_DELETED, changes["ABC"][0].change) + + +class TestWhatChangedForArrayChanges(TestBase): + def setUp(self): + super().setUp() + + def test_what_changed_for_array_value_changed(self): + """["a",1,"b"] -> ["a",2,"c"] produces 2 ARRAY_VALUE_CHANGED entries.""" + with self.store.open_session() as session: + session.store(Arr(arr=["a", 1, "b"]), "arr/1") + session.save_changes() + + with self.store.open_session() as session: + arr = session.load("arr/1", Arr) + arr.arr = ["a", 2, "c"] + + changes = session.advanced.what_changed_for(arr) + + self.assertEqual(2, len(changes)) + self.assertEqual(DocumentsChanges.ChangeType.ARRAY_VALUE_CHANGED, changes[0].change) + self.assertEqual(1, changes[0].field_old_value) + self.assertEqual(2, changes[0].field_new_value) + self.assertEqual(DocumentsChanges.ChangeType.ARRAY_VALUE_CHANGED, changes[1].change) + self.assertEqual("b", changes[1].field_old_value) + self.assertEqual("c", changes[1].field_new_value) + + def test_what_changed_for_array_value_added(self): + """["a",1,"b"] -> ["a",1,"b","c",2] produces 2 ARRAY_VALUE_ADDED entries.""" + with self.store.open_session() as session: + session.store(Arr(arr=["a", 1, "b"]), "arr/1") + session.save_changes() + + with self.store.open_session() as session: + arr = session.load("arr/1", Arr) + arr.arr = ["a", 1, "b", "c", 2] + + changes = session.advanced.what_changed_for(arr) + + self.assertEqual(2, len(changes)) + self.assertEqual(DocumentsChanges.ChangeType.ARRAY_VALUE_ADDED, changes[0].change) + self.assertIsNone(changes[0].field_old_value) + self.assertEqual("c", changes[0].field_new_value) + self.assertEqual(DocumentsChanges.ChangeType.ARRAY_VALUE_ADDED, changes[1].change) + self.assertIsNone(changes[1].field_old_value) + self.assertEqual(2, changes[1].field_new_value) + + def test_what_changed_for_array_value_removed(self): + """["a",1,"b"] -> ["a"] produces 2 ARRAY_VALUE_REMOVED entries.""" + with self.store.open_session() as session: + session.store(Arr(arr=["a", 1, "b"]), "arr/1") + session.save_changes() + + with self.store.open_session() as session: + arr = session.load("arr/1", Arr) + arr.arr = ["a"] + + changes = session.advanced.what_changed_for(arr) + + self.assertEqual(2, len(changes)) + self.assertEqual(DocumentsChanges.ChangeType.ARRAY_VALUE_REMOVED, changes[0].change) + self.assertEqual(1, changes[0].field_old_value) + self.assertIsNone(changes[0].field_new_value) + self.assertEqual(DocumentsChanges.ChangeType.ARRAY_VALUE_REMOVED, changes[1].change) + self.assertEqual("b", changes[1].field_old_value) + self.assertIsNone(changes[1].field_new_value)