From af61c7ea05ab4d0fde893c5c0dbc14481e29f472 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Wed, 3 Jan 2024 15:55:59 +0100 Subject: [PATCH 01/33] RDBC-775 Revisions --- ravendb/__init__.py | 2 +- ravendb/documents/commands/revisions.py | 116 ++++++++ ravendb/documents/operations/identities.py | 2 +- ravendb/documents/operations/revisions.py | 216 +++++++++++++++ .../operations/revisions/configuration.py | 7 - ravendb/documents/session/document_session.py | 22 +- .../in_memory_document_session_operations.py | 37 +++ .../session/document_session_revisions.py | 151 +++++++++++ .../session/operations/operations.py | 251 +++++++++++++++++- ravendb/json/result.py | 12 + ravendb/serverwide/database_record.py | 2 +- .../client_tests/revisions_tests}/__init__.py | 0 ravendb/tests/test_base.py | 21 ++ ravendb/tools/utils.py | 2 +- 14 files changed, 818 insertions(+), 23 deletions(-) create mode 100644 ravendb/documents/commands/revisions.py create mode 100644 ravendb/documents/operations/revisions.py delete mode 100644 ravendb/documents/operations/revisions/configuration.py create mode 100644 ravendb/documents/session/document_session_revisions.py rename ravendb/{documents/operations/revisions => tests/jvm_migrated_tests/client_tests/revisions_tests}/__init__.py (100%) diff --git a/ravendb/__init__.py b/ravendb/__init__.py index a7f3ef5e..443d92d2 100644 --- a/ravendb/__init__.py +++ b/ravendb/__init__.py @@ -120,7 +120,7 @@ ReplicationNode, ExternalReplicationBase, ) -from ravendb.documents.operations.revisions.configuration import ( +from ravendb.documents.operations.revisions import ( RevisionsCollectionConfiguration, RevisionsConfiguration, ) diff --git a/ravendb/documents/commands/revisions.py b/ravendb/documents/commands/revisions.py new file mode 100644 index 00000000..965f7d5f --- /dev/null +++ b/ravendb/documents/commands/revisions.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +import json +from datetime import datetime +from typing import List, Optional, TYPE_CHECKING + +import requests + +from ravendb.http.raven_command import RavenCommand +from ravendb.json.result import JsonArrayResult +from ravendb.tools.utils import Utils + +if TYPE_CHECKING: + from ravendb.http.server_node import ServerNode + + +class GetRevisionsCommand(RavenCommand[JsonArrayResult]): + def __init__( + self, + id_: str = None, + start: int = None, + page_size: int = None, + metadata_only: bool = None, + before: datetime = None, + change_vector: str = None, + change_vectors: List[str] = None, + ): + super().__init__(JsonArrayResult) + self._id = id_ + self._start = start + self._page_size = page_size + self._metadata_only = metadata_only + self._before = before + self._change_vector = change_vector + self._change_vectors = change_vectors + + @property + def change_vectors(self) -> Optional[List[str]]: + return self._change_vectors + + @property + def change_vector(self) -> Optional[str]: + return self._change_vector + + @property + def before(self) -> Optional[datetime]: + return self._before + + @property + def id(self) -> Optional[str]: + return self._id + + @classmethod + def from_change_vector(cls, change_vector: str, metadata_only: bool = False) -> GetRevisionsCommand: + return GetRevisionsCommand(change_vector=change_vector, metadata_only=metadata_only) + + @classmethod + def from_change_vectors(cls, change_vectors: List[str] = None, metadata_only: bool = False) -> GetRevisionsCommand: + return GetRevisionsCommand(change_vectors=change_vectors, metadata_only=metadata_only) + + @classmethod + def from_before(cls, id_: str, before: datetime) -> GetRevisionsCommand: + if id_ is None: + raise ValueError("Id cannot be None") + + @classmethod + def from_start_page( + cls, id_: str = None, start: int = None, page_size: int = None, metadata_only: bool = False + ) -> GetRevisionsCommand: + if id_ is None: + raise ValueError("Id cannot be None") + + return cls(id_=id_, start=start, page_size=page_size, metadata_only=metadata_only) + + def create_request(self, node: ServerNode) -> requests.Request: + path_builder = [node.url, "/databases/", node.database, "/revisions?"] + + self.get_request_query_string(path_builder) + return requests.Request("GET", "".join(path_builder)) + + def get_request_query_string(self, path_builder: List[str]) -> None: + if self._id is not None: + path_builder.append("&id=") + path_builder.append(Utils.escape(self._id)) + elif self._change_vector is not None: + path_builder.append("&changeVector=") + path_builder.append(Utils.escape(self._change_vector)) + elif self._change_vectors is not None: + for change_vector in self._change_vectors: + path_builder.append("&changeVector=") + path_builder.append(Utils.escape(change_vector)) + + if self._before is not None: + path_builder.append("&before") + path_builder.append(Utils.datetime_to_string(self._before)) + + if self._start is not None: + path_builder.append("&start=") + path_builder.append(str(self._start)) + + if self._page_size is not None: + path_builder.append("&pageSize=") + path_builder.append(str(self._page_size)) + + if self._metadata_only: + path_builder.append("&metadataOnly=true") + + def set_response(self, response: Optional[str], from_cache: bool) -> None: + if response is None: + self.result = None + return + + self.result = JsonArrayResult.from_json(json.loads(response)) + + def is_read_request(self) -> bool: + return True diff --git a/ravendb/documents/operations/identities.py b/ravendb/documents/operations/identities.py index a8a8826e..6afbb04a 100644 --- a/ravendb/documents/operations/identities.py +++ b/ravendb/documents/operations/identities.py @@ -28,7 +28,7 @@ def from_copy(cls, copy: RavenCommand[_T_Result]) -> Union[Broadcast, RavenComma copied = super().from_copy(copy) copied._raft_unique_request_id = copy._raft_unique_request_id - copied._id = copy._id + copied.id_ = copy._id return copied def is_read_request(self) -> bool: diff --git a/ravendb/documents/operations/revisions.py b/ravendb/documents/operations/revisions.py new file mode 100644 index 00000000..f2cac822 --- /dev/null +++ b/ravendb/documents/operations/revisions.py @@ -0,0 +1,216 @@ +from __future__ import annotations + +import datetime +import json +from datetime import timedelta +from typing import Dict, Any, Generic, TypeVar, List, Optional, Type, TYPE_CHECKING + +import requests + +from ravendb.documents.commands.revisions import GetRevisionsCommand +from ravendb.documents.operations.definitions import IOperation, MaintenanceOperation +from ravendb.http.raven_command import RavenCommand +from ravendb.util.util import RaftIdGenerator +from ravendb.http.topology import RaftCommand + +if TYPE_CHECKING: + from ravendb.http.http_cache import HttpCache + from ravendb import DocumentStore, ServerNode, EntityToJson + from ravendb.documents.conventions import DocumentConventions + + +_T = TypeVar("_T") + + +class RevisionsCollectionConfiguration: + def __init__( + self, + minimum_revisions_to_keep: int = None, + minimum_revisions_age_to_keep: timedelta = None, + disabled: bool = False, + purge_on_delete: bool = None, + maximum_revisions_to_delete_upon_document_creation: int = None, + ): + self.minimum_revisions_to_keep = minimum_revisions_to_keep + self.minimum_revisions_age_to_keep = minimum_revisions_age_to_keep + self.disabled = disabled + self.purge_on_delete = purge_on_delete + self.maximum_revisions_to_delete_upon_document_creation = maximum_revisions_to_delete_upon_document_creation + + @classmethod + def from_json(cls, json_dict: Dict[str, Any]) -> RevisionsCollectionConfiguration: + return cls( + json_dict["MinimumRevisionsToKeep"], + json_dict["MinimumRevisionAgeToKeep"], + json_dict["Disabled"], + json_dict["PurgeOnDelete"], + json_dict["MaximumRevisionsToDeleteUponDocumentUpdate"], + ) + + def to_json(self) -> Dict[str, Any]: + return { + "MinimumRevisionsToKeep": self.minimum_revisions_to_keep, + "MinimumRevisionAgeToKeep": self.minimum_revisions_age_to_keep, + "Disabled": self.disabled, + "PurgeOnDelete": self.purge_on_delete, + "MaximumRevisionsToDeleteUponDocumentUpdate": self.maximum_revisions_to_delete_upon_document_creation, + } + + +class RevisionsConfiguration: + def __init__( + self, + default_config: RevisionsCollectionConfiguration = None, + collections: Dict[str, RevisionsCollectionConfiguration] = None, + ): + self.default_config = default_config + self.collections = collections + + def to_json(self) -> Dict[str, Any]: + return { + "Default": self.default_config.to_json(), + "Collections": {key: value.to_json() for key, value in self.collections.items()} + if self.collections + else None, + } + + @classmethod + def from_json(cls, json_dict: Dict[str, Any]) -> RevisionsConfiguration: + return cls( + RevisionsCollectionConfiguration.from_json(json_dict["Default"]), + {key: RevisionsCollectionConfiguration.from_json(value) for key, value in json_dict["Collections"].items()}, + ) + + +class RevisionsResult(Generic[_T]): + def __init__(self, results: List[_T] = None, total_results: int = None): + self.results = results + self.total_results = total_results + + +class RevisionIncludeResult: + def __init__( + self, + Id: str = None, + change_vector: str = None, + before: datetime.datetime = None, + revision: Dict[str, Any] = None, + ): + self.Id = Id + self.change_vector = change_vector + self.before = before + self.revision = revision + + +class GetRevisionsOperation(Generic[_T], IOperation[RevisionsResult[_T]]): + class Parameters: + def __init__(self, id_: str = None, start: int = None, page_size: int = None): + self.id_ = id_ + self.start = start + self.page_size = page_size + + def validate(self): + if not self.id_: + raise ValueError("Id cannot be None") + + def __init__( + self, id_: str = None, object_type: Optional[Type[_T]] = dict, start: int = None, page_size: int = None + ): + parameters = self.Parameters(id_, start, page_size) + self._object_type = object_type + self._parameters = parameters + + @classmethod + def from_parameters(cls, parameters: Parameters, object_type: Type[_T] = None): + if parameters is None: + raise ValueError("Parameters cannot be None") + + parameters.validate() + + operation = cls() + operation._object_type = object_type + operation._parameters = parameters + + def get_command(self, store: DocumentStore, conventions: DocumentConventions, cache: HttpCache) -> RavenCommand[_T]: + return self.GetRevisionsResultCommand( + self._object_type, self._parameters.id_, self._parameters.start, self._parameters.page_size + ) + + class GetRevisionsResultCommand(RavenCommand[RevisionsResult[_T]]): + def __init__(self, object_type: Optional[Type[_T]], id_: str = None, start: int = None, page_size: int = None): + super().__init__(RevisionsResult[_T]) + self._object_type = object_type + self._cmd = GetRevisionsCommand(id_, start, page_size) + + def is_read_request(self) -> bool: + return True + + def create_request(self, node: ServerNode) -> requests.Request: + return self._cmd.create_request(node) + + def set_response(self, response: Optional[str], from_cache: bool) -> None: + if response is None: + return + + response_dict = json.loads(response) + if "Results" not in response_dict: + return + + revisions = response_dict["Results"] + total = response_dict["TotalResults"] + + results = [] + for revision in revisions: + if not revision: + continue + + entity = EntityToJson.convert_to_entity_static( + revision, self._object_type, DocumentConventions.default_conventions() + ) + results.append(entity) + + result = RevisionsResult(results, total) + self.result = result + + +class ConfigureRevisionsOperationResult: + def __init__(self, raft_command_index: int = None): + self.raft_command_index = raft_command_index + + @classmethod + def from_json(cls, json_dict: Dict[str, Any]) -> ConfigureRevisionsOperationResult: + return cls(json_dict["RaftCommandIndex"]) + + +class ConfigureRevisionsOperation(MaintenanceOperation[ConfigureRevisionsOperationResult]): + def __init__(self, configuration: RevisionsConfiguration): + if configuration is None: + raise ValueError("Configuration cannot be None") + self._configuration = configuration + + def get_command(self, conventions: "DocumentConventions") -> "RavenCommand[ConfigureRevisionsOperationResult]": + return self.ConfigureRevisionsCommand(self._configuration) + + class ConfigureRevisionsCommand(RavenCommand[ConfigureRevisionsOperationResult], RaftCommand): + def __init__(self, configuration: RevisionsConfiguration): + super().__init__(ConfigureRevisionsOperationResult) + self._configuration = configuration + + def is_read_request(self) -> bool: + return False + + def create_request(self, node: ServerNode) -> requests.Request: + url = f"{node.url}/databases/{node.database}/admin/revisions/config" + + request = requests.Request("POST", url) + request.data = self._configuration.to_json() + return request + + def set_response(self, response: Optional[str], from_cache: bool) -> None: + if response is None: + self._throw_invalid_response() + + self.result = ConfigureRevisionsOperationResult.from_json(json.loads(response)) + + def get_raft_unique_request_id(self) -> str: + return RaftIdGenerator().new_id() diff --git a/ravendb/documents/operations/revisions/configuration.py b/ravendb/documents/operations/revisions/configuration.py deleted file mode 100644 index 752aed96..00000000 --- a/ravendb/documents/operations/revisions/configuration.py +++ /dev/null @@ -1,7 +0,0 @@ -# todo: implement -class RevisionsConfiguration: - pass - - -class RevisionsCollectionConfiguration: - pass diff --git a/ravendb/documents/session/document_session.py b/ravendb/documents/session/document_session.py index b63bc10a..642f1d64 100644 --- a/ravendb/documents/session/document_session.py +++ b/ravendb/documents/session/document_session.py @@ -10,6 +10,7 @@ import uuid from typing import Union, Callable, TYPE_CHECKING, Optional, Dict, List, Type, TypeVar, Tuple, Generic, Set +from ravendb.documents.session.document_session_revisions import DocumentSessionRevisions from ravendb.primitives import constants from ravendb.primitives.constants import int_max from ravendb.documents.operations.counters import CounterOperation, CounterOperationType, GetCountersOperation @@ -112,10 +113,11 @@ def __init__(self, store: DocumentStore, key: uuid.UUID, options: SessionOptions super().__init__(store, key, options) self._advanced: DocumentSession._Advanced = DocumentSession._Advanced(self) - self.__document_query_generator: DocumentSession._DocumentQueryGenerator = ( + self._document_query_generator: DocumentSession._DocumentQueryGenerator = ( DocumentSession._DocumentQueryGenerator(self) ) - self.__cluster_transaction: Union[None, ClusterTransactionOperations] = None + self._cluster_transaction: Optional[ClusterTransactionOperations] = None + self._revisions: Optional[DocumentSessionRevisions] = None @property def advanced(self): @@ -127,13 +129,13 @@ def _lazily(self): @property def cluster_transaction(self) -> IClusterTransactionOperations: - if self.__cluster_transaction is None: - self.__cluster_transaction = ClusterTransactionOperations(self) - return self.__cluster_transaction + if self._cluster_transaction is None: + self._cluster_transaction = ClusterTransactionOperations(self) + return self._cluster_transaction @property def has_cluster_session(self) -> bool: - return self.__cluster_transaction is not None + return self._cluster_transaction is not None @property def operations(self) -> OperationExecutor: @@ -157,7 +159,7 @@ def save_changes(self) -> None: save_changes_operation.set_result(command.result) def _has_cluster_session(self) -> bool: - return self.__cluster_transaction is not None + return self._cluster_transaction is not None def _clear_cluster_session(self) -> None: if not self._has_cluster_session(): @@ -568,6 +570,12 @@ def attachments(self): self.__attachment = self._Attachment(self._session) return self.__attachment + @property + def revisions(self): + if self._session._revisions is None: + self._session._revisions = DocumentSessionRevisions(self._session) + return self._session._revisions + @property def cluster_transaction(self) -> IClusterTransactionOperations: return self._session.cluster_transaction 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 90bbaa63..55148143 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 @@ -59,6 +59,7 @@ if TYPE_CHECKING: from ravendb.documents.operations.lazy.lazy_operation import LazyOperation from ravendb.documents.store.definition import DocumentStore + from ravendb.http.request_executor import RequestExecutor class RefEq: @@ -456,6 +457,10 @@ def __init__(self, store: "DocumentStore", key: uuid.UUID, options: SessionOptio self._known_missing_ids = CaseInsensitiveSet() self._documents_by_id = DocumentsByIdHolder() self._included_documents_by_id = CaseInsensitiveDict() + self.include_revisions_by_change_vector = CaseInsensitiveDict() + self.include_revisions_by_date_time_before: Optional[ + Dict[str, Dict[datetime.datetime, DocumentInfo]] + ] = CaseInsensitiveDict() self._documents_by_entity: DocumentsByEntityHolder = DocumentsByEntityHolder() self._counters_by_doc_id: Dict[str, List[Dict[str, int]]] = CaseInsensitiveDict() @@ -640,6 +645,18 @@ def time_series_by_doc_id(self) -> Dict[str, Dict[str, List[TimeSeriesRangeResul def number_of_requests(self) -> int: return self._number_of_requests + @property + def document_store(self) -> DocumentStore: + return self._document_store + + @property + def request_executor(self) -> RequestExecutor: + return self._request_executor + + @property + def ids_for_creating_forced_revisions(self): + return self._ids_for_creating_forced_revisions + @property def no_tracking(self) -> bool: return self._no_tracking @@ -1698,6 +1715,26 @@ def __deserialize_from_transformer( ) -> _T: return self.entity_to_json.convert_to_entity(object_type, key, document, track_entity) + def check_if_all_change_vectors_are_already_included(self, change_vectors: List[str]) -> bool: + if self.include_revisions_by_change_vector is None: + return False + + for cv in change_vectors: + if cv not in self.include_revisions_by_change_vector: + return False + + return True + + def check_if_revisions_by_date_time_before_already_included(self, id_: str, date_time: datetime) -> bool: + if self.include_revisions_by_date_time_before is None: + return False + + dictionary_date_time_to_document = self.include_revisions_by_date_time_before.get(id_) + if dictionary_date_time_to_document is not None: + return date_time in dictionary_date_time_to_document + + return False + def check_if_id_already_included(self, ids: List[str], includes: Union[List[List], List[str]]) -> bool: if includes and not isinstance(includes[0], str): return self.check_if_id_already_included(ids, [arr[1] for arr in includes]) diff --git a/ravendb/documents/session/document_session_revisions.py b/ravendb/documents/session/document_session_revisions.py new file mode 100644 index 00000000..62c02554 --- /dev/null +++ b/ravendb/documents/session/document_session_revisions.py @@ -0,0 +1,151 @@ +import datetime +from typing import TYPE_CHECKING, TypeVar, Type, List, Dict + +from ravendb.documents.session.misc import ForceRevisionStrategy +from ravendb.documents.session.operations.operations import GetRevisionOperation, GetRevisionsCountOperation + +if TYPE_CHECKING: + from ravendb.documents.session.document_session_operations.in_memory_document_session_operations import ( + InMemoryDocumentSessionOperations, + ) + from ravendb.documents.commands.batches import CommandData + from ravendb.json.metadata_as_dictionary import MetadataAsDictionary + +_T = TypeVar("_T") + + +class AdvancedSessionExtensionBase: + def __init__(self, session: "InMemoryDocumentSessionOperations"): + self.session = session + self.request_executor = session.request_executor + self.session_info = session.session_info + self.document_store = session.document_store + self.deferred_commands_map = session.deferred_commands_map + self.documents_by_id = session.documents_by_id + + def defer(self, *commands: "CommandData") -> None: + self.session.defer(*commands) + + +class DocumentSessionRevisionsBase(AdvancedSessionExtensionBase): + def __init__(self, session: "InMemoryDocumentSessionOperations"): + super().__init__(session) + + def force_revision_creation_for( + self, entity: _T, strategy: ForceRevisionStrategy = ForceRevisionStrategy.BEFORE + ) -> None: + if entity is None: + raise ValueError("Entity cannot be None") + + document_info = self.session.documents_by_entity.get(entity, None) + if document_info is None: + raise RuntimeError( + "Cannot create a revision for the requested entity because it is not tracked by the session" + ) + + self._add_id_to_list(document_info.key, strategy) + + def force_revision_creation_for_id( + self, id: str, strategy: ForceRevisionStrategy = ForceRevisionStrategy.BEFORE + ) -> None: + self._add_id_to_list(id, strategy) + + def _add_id_to_list(self, id_: str, requested_strategy: ForceRevisionStrategy) -> None: + if not id_: + raise ValueError("Id cannot be None or empty") + + existing_strategy = self.session.ids_for_creating_forced_revisions.get(id_) + id_already_added = existing_strategy is not None + + if id_already_added and existing_strategy != requested_strategy: + raise RuntimeError( + f"A request for creating a revision was already made for document {id_} in the current session " + f"but with a different force strategy. " + f"New strategy requested: {requested_strategy}. " + f"Previous strategy: {existing_strategy}." + ) + + if not id_already_added: + self.session.ids_for_creating_forced_revisions[id_] = requested_strategy + + +class DocumentSessionRevisions(DocumentSessionRevisionsBase): + def __init__(self, session: "InMemoryDocumentSessionOperations"): + super().__init__(session) + + # def lazily(self) -> LazyRevisionOperations: + # return LazyRevisionOperations(self.session) + + def get_for(self, id_: str, object_type: Type[_T] = None, start: int = 0, page_size: int = 25) -> List[_T]: + operation = GetRevisionOperation.from_start_page(self.session, id_, start, page_size) + command = operation.create_request() + + if command is None: + return operation.get_revisions_for(object_type) + + if self.session_info is not None: + self.session_info.increment_request_count() + + self.request_executor.execute_command(command, self.session_info) + operation.set_result(command.result) + return operation.get_revisions_for(object_type) + + def get_metadata_for(self, id_: str, start: int = 0, page_size: int = 25) -> List["MetadataAsDictionary"]: + operation = GetRevisionOperation.from_start_page(self.session, id_, start, page_size, True) + command = operation.create_request() + if command is None: + return operation.get_revisions_metadata_for() + + if self.session_info is not None: + self.session_info.increment_request_count() + + self.request_executor.execute_command(command, self.session_info) + operation.set_result(command.result) + return operation.get_revisions_metadata_for() + + def get_by_change_vector(self, change_vector: str, object_type: Type[_T] = None) -> _T: + operation = GetRevisionOperation.from_change_vector(self.session, change_vector) + command = operation.create_request() + if command is None: + return operation.get_revision(object_type) + + if self.session_info is not None: + self.session_info.increment_request_count() + + self.request_executor.execute_command(command, self.session_info) + operation.set_result(command.result) + return operation.get_revision(object_type) + + def get_by_change_vectors(self, change_vectors: List[str], object_type: Type[_T] = None) -> Dict[str, _T]: + operation = GetRevisionOperation.from_change_vectors(self.session, change_vectors) + command = operation.create_request() + if command is None: + return operation.get_revisions(object_type) + + if self.session_info is not None: + self.session_info.increment_request_count() + + self.request_executor.execute_command(command, self.session_info) + operation.set_result(command.result) + return operation.get_revisions(object_type) + + def get_by_before_date(self, id_: str, before_date: datetime.datetime, object_type: Type[_T] = None) -> _T: + operation = GetRevisionOperation.from_before(self.session, id_, before_date) + command = operation.create_request() + if command is None: + return operation.get_revision(object_type) + + if self.session_info is not None: + self.session_info.increment_request_count() + + self.request_executor.execute_command(command, self.session_info) + operation.set_result(command.result) + return operation.get_revision(object_type) + + def get_count_for(self, id_: str) -> int: + operation = GetRevisionsCountOperation(id_) + command = operation.create_request() + if self.session_info is not None: + self.session_info.increment_request_count() + self.request_executor.execute_command(command, self.session_info) + return command.result diff --git a/ravendb/documents/session/operations/operations.py b/ravendb/documents/session/operations/operations.py index 48473ffc..5d4dc6d4 100644 --- a/ravendb/documents/session/operations/operations.py +++ b/ravendb/documents/session/operations/operations.py @@ -1,11 +1,28 @@ +from __future__ import annotations + +import json import logging -from typing import Union, List, Type, TypeVar -from ravendb.documents.commands.crud import GetDocumentsCommand, GetDocumentsResult +from datetime import datetime +from typing import Union, List, Type, TypeVar, Optional, Dict, Any, TYPE_CHECKING + +import requests + +from ravendb.http.server_node import ServerNode from ravendb.documents.commands.multi_get import GetRequest, MultiGetCommand +from ravendb.documents.commands.revisions import GetRevisionsCommand from ravendb.documents.session.document_info import DocumentInfo -from ravendb.documents.session.document_session_operations.in_memory_document_session_operations import ( - InMemoryDocumentSessionOperations, -) + +from ravendb.primitives import constants +from ravendb.tools.utils import Utils, CaseInsensitiveDict +from ravendb.json.metadata_as_dictionary import MetadataAsDictionary +from ravendb.http.raven_command import RavenCommand + +if TYPE_CHECKING: + from ravendb.json.result import JsonArrayResult + from ravendb.documents.session.document_session_operations.in_memory_document_session_operations import ( + InMemoryDocumentSessionOperations, + ) + from ravendb.documents.commands.crud import GetDocumentsCommand, GetDocumentsResult _T = TypeVar("_T") @@ -105,3 +122,227 @@ def __get_document(self, object_type: Type[_T], key: str) -> _T: return self.__session.track_entity_document_info(object_type, doc) return None + + +class GetRevisionOperation: + def __init__(self, session: InMemoryDocumentSessionOperations = None): + if session is None: + raise ValueError("Session cannot be None") + self._session = session + self._command: Optional[GetRevisionsCommand] = None + self._result: Optional[JsonArrayResult] = None + + @classmethod + def from_start_page( + cls, + session: InMemoryDocumentSessionOperations, + id_: str, + start: int, + page_size: int, + metadata_only: bool = False, + ) -> GetRevisionOperation: + self = cls(session) + if id_ is None: + raise ValueError("Id cannot be None") + + self._session = session + self._command = GetRevisionsCommand.from_start_page(id_, start, page_size, metadata_only) + return self + + @classmethod + def from_before( + cls, session: InMemoryDocumentSessionOperations, id_: str, before: datetime + ) -> GetRevisionOperation: + self = cls(session) + self._command = GetRevisionsCommand.from_before(id_, before) + return self + + @classmethod + def from_change_vector(cls, session: InMemoryDocumentSessionOperations, change_vector: str) -> GetRevisionOperation: + self = cls(session) + self._command = GetRevisionsCommand.from_change_vector(change_vector) + return self + + @classmethod + def from_change_vectors( + cls, session: InMemoryDocumentSessionOperations, change_vectors: List[str] + ) -> GetRevisionOperation: + self = cls(session) + self._command = GetRevisionsCommand.from_change_vectors(change_vectors) + + def create_request(self) -> GetRevisionsCommand: + if self._command.change_vectors is not None: + return ( + None + if self._session.check_if_all_change_vectors_are_already_included(self._command.change_vectors) + else self._command + ) + + if self._command.change_vector is not None: + return ( + None + if self._session.check_if_all_change_vectors_are_already_included([self._command.change_vector]) + else self._command + ) + + if self._command.before is not None: + return ( + None + if self._session.check_if_revisions_by_date_time_before_already_included( + self._command.id, self._command.before + ) + else self._command + ) + + return self._command + + def set_result(self, result: JsonArrayResult) -> None: + self._result = result + + @property + def command(self) -> GetRevisionsCommand: + return self._command + + def _get_revision(self, document: Dict[str, Any], object_type: Type[_T] = dict) -> _T: + if document is None: + return Utils.get_default_value(object_type) + + metadata = None + id_ = None + if constants.Documents.Metadata.KEY in document: + metadata = document.get(constants.Documents.Metadata.KEY) + id_node = metadata.get(constants.Documents.Metadata.ID, None) + if id_node is not None: + id_ = id_node + + change_vector = None + if metadata is not None and constants.Documents.Metadata.CHANGE_VECTOR in metadata: + change_vector_node = metadata.get(constants.Documents.Metadata.CHANGE_VECTOR, None) + if change_vector_node is not None: + change_vector = change_vector_node + + entity = self._session.entity_to_json.convert_to_entity( + object_type, id_, document, not self._session.no_tracking + ) + document_info = DocumentInfo() + document_info.key = id_ + document_info.change_vector = change_vector + document_info.document = document + document_info.metadata = metadata + document_info.entity = entity + self._session.documents_by_entity[entity] = document_info + + return entity + + def get_revisions_for(self, object_type: Type[_T]) -> List[_T]: + results_count = len(self._result.results) + results = [] + for i in range(results_count): + document = self._result.results[i] + results.append(self._get_revision(document, object_type)) + + return results + + def get_revisions_metadata_for(self) -> List[MetadataAsDictionary]: + results_count = len(self._result.results) + results = [] + for i in range(results_count): + document = self._result.results[i] + + metadata = None + if constants.Documents.Metadata.KEY in document: + metadata = document.get(constants.Documents.Metadata.KEY) + + results.append(MetadataAsDictionary(metadata)) + + return results + + def get_revision(self, object_type: Type[_T]) -> _T: + if self._result is None: + revision: DocumentInfo + + if self._command.change_vectors is not None: + for change_vector in self._command.change_vectors: + revision = self._session.include_revisions_by_change_vector.get(change_vector, None) + if revision is not None: + return self._get_revision(revision.document, object_type) + + if self._command.change_vector is not None and self._session.include_revisions_by_change_vector is not None: + revision = self._session.include_revisions_by_change_vector.get(self._command.change_vector, None) + if revision is not None: + return self._get_revision(revision.document, object_type) + + if self._command.before is not None and self._session.include_revisions_by_date_time_before is not None: + dictionary_date_time_to_document = self._session.include_revisions_by_date_time_before.get( + self.command.id + ) + if dictionary_date_time_to_document is not None: + revision = dictionary_date_time_to_document.get(self._command.before) + if revision is not None: + return self._get_revision(revision.document, object_type) + + return Utils.get_default_value(object_type) + + document = self._result.results[0] + return self._get_revision(document, object_type) + + def get_revisions(self, object_type: Type[_T]) -> Dict[str, _T]: + results = CaseInsensitiveDict() + + if self._result is None: + for change_vector in self._command.change_vectors: + revision = self._session.include_revisions_by_change_vector.get(change_vector) + if revision is not None: + results[change_vector] = self._get_revision(revision.document, object_type) + + return results + + for i in range(len(self._command.change_vectors)): + change_vector = self._command.change_vectors[i] + if change_vector is None: + continue + + json_dict = self._result.results[i] + results[change_vector] = self._get_revision(json_dict, object_type) + + return results + + +class DocumentRevisionsCount: + def __init__(self, revisions_count: int = None): + self.revisions_count = revisions_count + + @classmethod + def from_json(cls, json_dict: Dict) -> DocumentRevisionsCount: + return cls(json_dict["RevisionsCount"]) + + +class GetRevisionsCountOperation: + def __init__(self, doc_id: str): + self._doc_id = doc_id + + def create_request(self) -> RavenCommand[int]: + return self.GetRevisionsCountCommand(self._doc_id) + + class GetRevisionsCountCommand(RavenCommand[int]): + def __init__(self, id_: str): + super().__init__(int) + if id_ is None: + raise ValueError("Id cannot be None") + + self._id = id_ + + def create_request(self, node: ServerNode) -> requests.Request: + return requests.Request( + "GET", f"{node.url}/databases/{node.database}/revisions/count?&id={Utils.quote_key(self._id)}" + ) + + def set_response(self, response: Optional[str], from_cache: bool) -> None: + if response is None: + self.result = 0 + return + + self.result = DocumentRevisionsCount.from_json(json.loads(response)).revisions_count + + def is_read_request(self) -> bool: + return True diff --git a/ravendb/json/result.py b/ravendb/json/result.py index 3fc6f42b..af3888fb 100644 --- a/ravendb/json/result.py +++ b/ravendb/json/result.py @@ -1,4 +1,16 @@ +from typing import List, Any, Dict + + class BatchCommandResult: def __init__(self, results, transaction_index): self.results: [None, list] = results self.transaction_index: [None, int] = transaction_index + + +class JsonArrayResult: + def __init__(self, results: List): + self.results = results + + @classmethod + def from_json(cls, json_dict: Dict[str, Any]): + return cls(json_dict["Results"]) diff --git a/ravendb/serverwide/database_record.py b/ravendb/serverwide/database_record.py index 54358f6d..e741fe66 100644 --- a/ravendb/serverwide/database_record.py +++ b/ravendb/serverwide/database_record.py @@ -21,7 +21,7 @@ PullReplicationDefinition, PullReplicationAsSink, ) -from ravendb.documents.operations.revisions.configuration import ( +from ravendb.documents.operations.revisions import ( RevisionsConfiguration, RevisionsCollectionConfiguration, ) diff --git a/ravendb/documents/operations/revisions/__init__.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/__init__.py similarity index 100% rename from ravendb/documents/operations/revisions/__init__.py rename to ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/__init__.py diff --git a/ravendb/tests/test_base.py b/ravendb/tests/test_base.py index 9162daba..7546b114 100644 --- a/ravendb/tests/test_base.py +++ b/ravendb/tests/test_base.py @@ -9,6 +9,13 @@ from subprocess import Popen from typing import Iterable, List, Union, Optional, Set from datetime import timedelta + +from ravendb.documents.operations.revisions import ( + ConfigureRevisionsOperationResult, + RevisionsConfiguration, + RevisionsCollectionConfiguration, + ConfigureRevisionsOperation, +) from ravendb.primitives import constants from ravendb.documents.operations.indexes import GetIndexErrorsOperation from ravendb.exceptions.exceptions import DatabaseDoesNotExistException @@ -437,3 +444,17 @@ def assertRaisesWithMessage(self, func, exception, msg, *args, **kwargs): def assertSequenceContainsElements(self, sequence, *args): for arg in args: self.assertIn(arg, sequence) + + @staticmethod + def setup_revisions( + store: DocumentStore, purge_on_delete: bool = None, minimum_revisions_to_keep: int = None + ) -> ConfigureRevisionsOperationResult: + revisions_configuration = RevisionsConfiguration() + default_collection = RevisionsCollectionConfiguration() + default_collection.purge_on_delete = purge_on_delete + default_collection.minimum_revisions_to_keep = minimum_revisions_to_keep + + revisions_configuration.default_config = default_collection + operation = ConfigureRevisionsOperation(revisions_configuration) + + return store.maintenance.send(operation) diff --git a/ravendb/tools/utils.py b/ravendb/tools/utils.py index 168d9d25..4529a646 100644 --- a/ravendb/tools/utils.py +++ b/ravendb/tools/utils.py @@ -762,7 +762,7 @@ def __escape_internal(term: str, wild_cards: Collection[str] = None, make_phrase elif ch == " " or ch == "\t": if make_phrase: - return '"{0}"'.format(Utils.escape(term, allow_wild_cards, False)) + return f'"{Utils.escape(term, allow_wild_cards, False)}"' i += 1 if length > start: From 9c1c8c5a57f7e4463a6765e47e9490c814141b7d Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Wed, 3 Jan 2024 15:57:58 +0100 Subject: [PATCH 02/33] RDBC-775 RevisionsTest::revisions --- .../revisions_tests/test_revisions.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py new file mode 100644 index 00000000..983d27e2 --- /dev/null +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py @@ -0,0 +1,54 @@ +from time import sleep + +from ravendb.infrastructure.entities import User +from ravendb.primitives import constants +from ravendb.tests.test_base import TestBase + + +class TestRevisions(TestBase): + def setUp(self): + super().setUp() + + def test_revisions(self): + TestBase.setup_revisions(self.store, False, 4) + + for i in range(4): + with self.store.open_session() as session: + user = User(name=f"user{i+1}") + session.store(user, "users/1") + session.save_changes() + + sleep(2) + with self.store.open_session() as session: + all_revisions = session.advanced.revisions.get_for("users/1", User) + self.assertEqual(4, len(all_revisions)) + self.assertEqual(["user4", "user3", "user2", "user1"], [x.name for x in all_revisions]) + + sleep(2) + revisions_skip_first = session.advanced.revisions.get_for("users/1", User, 1) + self.assertEqual(3, len(revisions_skip_first)) + self.assertEqual(["user3", "user2", "user1"], [x.name for x in revisions_skip_first]) + + sleep(2) + revisions_skip_first_take_two = session.advanced.revisions.get_for("users/1", User, 1, 2) + self.assertEqual(2, len(revisions_skip_first_take_two)) + self.assertEqual(["user3", "user2"], [x.name for x in revisions_skip_first_take_two]) + + sleep(2) + all_metadata = session.advanced.revisions.get_metadata_for("users/1") + self.assertEqual(4, len(all_metadata)) + + sleep(2) + metadata_skip_first = session.advanced.revisions.get_metadata_for("users/1", 1) + self.assertEqual(3, len(metadata_skip_first)) + + sleep(2) + metadata_skip_first_take_two = session.advanced.revisions.get_metadata_for("users/1", 1, 2) + self.assertEqual(2, len(metadata_skip_first_take_two)) + + sleep(2) + + user = session.advanced.revisions.get_by_change_vector( + metadata_skip_first[0].metadata.get(constants.Documents.Metadata.CHANGE_VECTOR), User + ) + self.assertEqual("user3", user.name) From 4818dd9d151a31bf441c7e914dd6b526d65ffc7b Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Wed, 3 Jan 2024 16:34:44 +0100 Subject: [PATCH 03/33] RDBC-775 Small imports fix --- ravendb/documents/session/operations/operations.py | 3 ++- ravendb/tests/raven_commands_tests/test_get_by_prefix.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ravendb/documents/session/operations/operations.py b/ravendb/documents/session/operations/operations.py index 5d4dc6d4..b217ceb2 100644 --- a/ravendb/documents/session/operations/operations.py +++ b/ravendb/documents/session/operations/operations.py @@ -16,13 +16,14 @@ from ravendb.tools.utils import Utils, CaseInsensitiveDict from ravendb.json.metadata_as_dictionary import MetadataAsDictionary from ravendb.http.raven_command import RavenCommand +from ravendb.documents.commands.crud import GetDocumentsCommand if TYPE_CHECKING: from ravendb.json.result import JsonArrayResult from ravendb.documents.session.document_session_operations.in_memory_document_session_operations import ( InMemoryDocumentSessionOperations, ) - from ravendb.documents.commands.crud import GetDocumentsCommand, GetDocumentsResult + from ravendb.documents.commands.crud import GetDocumentsResult _T = TypeVar("_T") diff --git a/ravendb/tests/raven_commands_tests/test_get_by_prefix.py b/ravendb/tests/raven_commands_tests/test_get_by_prefix.py index 95770d82..a76ddb25 100644 --- a/ravendb/tests/raven_commands_tests/test_get_by_prefix.py +++ b/ravendb/tests/raven_commands_tests/test_get_by_prefix.py @@ -1,6 +1,6 @@ import unittest -from ravendb.documents.commands.crud import PutDocumentCommand, GetDocumentsCommand +from ravendb.documents.commands.crud import PutDocumentCommand from ravendb.tests.test_base import TestBase From 9faacd6abb9aac8ca7eaa79b68de8556efa64980 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 4 Jan 2024 12:52:05 +0100 Subject: [PATCH 04/33] RDBC-775 ForceRevisionCreationTest::hasRevisionsFlagIsCreatedWhenForcingRevisionForDocumentThatHasNoRevisionsYet --- ravendb/documents/operations/batch.py | 8 +-- .../test_force_revision_creation.py | 63 +++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py diff --git a/ravendb/documents/operations/batch.py b/ravendb/documents/operations/batch.py index ac141204..2d7f7a94 100644 --- a/ravendb/documents/operations/batch.py +++ b/ravendb/documents/operations/batch.py @@ -239,20 +239,20 @@ def __handle_force_revision_creation(self, batch_result: dict) -> None: # no forced revision was created...nothing to update return - key = self.__get_string_field( - batch_result, CommandType.FORCE_REVISION_CREATION, constants.Documents.Metadata.KEY + id_ = self.__get_string_field( + batch_result, CommandType.FORCE_REVISION_CREATION, constants.Documents.Metadata.ID ) change_vector = self.__get_string_field( batch_result, CommandType.FORCE_REVISION_CREATION, constants.Documents.Metadata.CHANGE_VECTOR ) - document_info = self.__session._documents_by_id.get(key) + document_info = self.__session._documents_by_id.get(id_) if not document_info: return document_info.change_vector = change_vector - self.__handle_metadata_modifications(document_info, batch_result, key, change_vector) + self.__handle_metadata_modifications(document_info, batch_result, id_, change_vector) self.__session.__on_after_save_changes(self.__session, document_info.key, document_info.entity) def __handle_put(self, index: int, batch_result: dict, is_deferred: bool) -> None: diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py new file mode 100644 index 00000000..61671dc8 --- /dev/null +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py @@ -0,0 +1,63 @@ +from ravendb.infrastructure.orders import Company +from ravendb.tests.test_base import TestBase + + +class TestForceRevisionCreation(TestBase): + def setUp(self): + super().setUp() + + def test_has_revisions_flag_is_created_when_forcing_revision_for_document_that_has_no_revisions_yet(self): + company1id = "" + company2id = "" + + with self.store.open_session() as session: + company1 = Company() + company1.name = "HR1" + + company2 = Company() + company2.name = "HR2" + + session.store(company1) + session.store(company2) + + session.save_changes() + + company1id = company1.Id + company2id = company2.Id + + revisions_count = len(session.advanced.revisions.get_for(company1.Id, Company)) + self.assertEqual(0, revisions_count) + + revisions_count = len(session.advanced.revisions.get_for(company2.Id, Company)) + self.assertEqual(0, revisions_count) + + with self.store.open_session() as session: + # Force revision with no changes on document + session.advanced.revisions.force_revision_creation_for_id(company1id) + + # Force revision with changes on document + session.advanced.revisions.force_revision_creation_for_id(company2id) + company2 = session.load(company2id, Company) + company2.name = "HR2 New Name" + + session.save_changes() + + with self.store.open_session() as session: + revisions = session.advanced.revisions.get_for(company1id, Company) + revisions_count = len(revisions) + self.assertEqual(1, revisions_count) + self.assertEqual("HR1", revisions[0].name) + + revisions = session.advanced.revisions.get_for(company2id, Company) + revisions_count = len(revisions) + self.assertEqual(1, revisions_count) + self.assertEqual("HR2", revisions[0].name) + + # Assert that HasRevisions flag was created on both documents + company = session.load(company1id, Company) + metadata = session.advanced.get_metadata_for(company) + self.assertEqual("HasRevisions", metadata["@flags"]) + + company = session.load(company2id, Company) + metadata = session.advanced.get_metadata_for(company) + self.assertEqual("HasRevisions", metadata["@flags"]) From f4bc1ea3c6d921434f6fb4e8a3fae1325ae58770 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 4 Jan 2024 13:11:55 +0100 Subject: [PATCH 05/33] RDBC-775 ForceRevisionCreationTest::forceRevisionCreationForTrackedEntityWithChangesByEntity --- .../test_force_revision_creation.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py index 61671dc8..0bdb9fb8 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py @@ -61,3 +61,35 @@ def test_has_revisions_flag_is_created_when_forcing_revision_for_document_that_h company = session.load(company2id, Company) metadata = session.advanced.get_metadata_for(company) self.assertEqual("HasRevisions", metadata["@flags"]) + + def test_force_revision_creation_for_tracked_entity_with_changes_by_entity(self): + company_id = "" + + # 1. Store document + with self.store.open_session() as session: + company = Company() + company.name = "HR" + session.store(company) + session.save_changes() + + company_id = company.Id + + revisions_count = len(session.advanced.revisions.get_for(company.Id, Company)) + self.assertEqual(0, revisions_count) + + # 2. Load, Make changes & Save + with self.store.open_session() as session: + company = session.load(company_id, Company) + company.name = "HR V2" + + session.advanced.revisions.force_revision_creation_for(company) + session.save_changes() + + revisions = session.advanced.revisions.get_for(company.Id, Company) + revisions_count = len(revisions) + + self.assertEqual(1, revisions_count) + + # Assert revision contains the value 'Before' the changes... + # ('Before' is the default force revision creation strategy) + self.assertEqual("HR", revisions[0].name) From f1e4786d1d53366c8b4fcfa8331c940abbed2e57 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 4 Jan 2024 13:37:30 +0100 Subject: [PATCH 06/33] RDBC-775 ForceRevisionCreationTest::forceRevisionCreationAcrossMultipleSessions --- ravendb/documents/operations/batch.py | 305 +++++++++--------- .../test_force_revision_creation.py | 67 ++++ 2 files changed, 222 insertions(+), 150 deletions(-) diff --git a/ravendb/documents/operations/batch.py b/ravendb/documents/operations/batch.py index 2d7f7a94..fc118008 100644 --- a/ravendb/documents/operations/batch.py +++ b/ravendb/documents/operations/batch.py @@ -19,40 +19,40 @@ class BatchOperation: def __init__(self, session: "InMemoryDocumentSessionOperations"): - self.__session = session - self.__entities: List[object] = [] - self.__session_commands_count: Union[None, int] = None - self.__all_commands_count: Union[None, int] = None - self.__on_successful_request: Union[ + self._session = session + self._entities: List[object] = [] + self._session_commands_count: Union[None, int] = None + self._all_commands_count: Union[None, int] = None + self._on_successful_request: Union[ None, "InMemoryDocumentSessionOperations.SaveChangesData.ActionsToRunOnSuccess" ] = None - self.__modifications: Union[None, Dict[str, DocumentInfo]] = None + self._modifications: Union[None, Dict[str, DocumentInfo]] = None def create_request(self) -> Union[None, SingleNodeBatchCommand]: - result = self.__session.prepare_for_save_changes() - self.__on_successful_request = result.on_success - self.__session_commands_count = len(result.session_commands) + result = self._session.prepare_for_save_changes() + self._on_successful_request = result.on_success + self._session_commands_count = len(result.session_commands) result.session_commands.extend(result.deferred_commands) - self.__session.validate_cluster_transaction(result) + self._session.validate_cluster_transaction(result) - self.__all_commands_count = len(result.session_commands) + self._all_commands_count = len(result.session_commands) - if self.__all_commands_count == 0: + if self._all_commands_count == 0: return None - self.__session.increment_requests_count() + self._session.increment_requests_count() - self.__entities = result.entities + self._entities = result.entities - if self.__session.transaction_mode == TransactionMode.CLUSTER_WIDE: + if self._session.transaction_mode == TransactionMode.CLUSTER_WIDE: return ClusterWideBatchCommand( - self.__session.conventions, + self._session.conventions, result.session_commands, result.options, - self.__session.disable_atomic_document_writes_in_cluster_wide_transaction, + self._session.disable_atomic_document_writes_in_cluster_wide_transaction, ) - return SingleNodeBatchCommand(self.__session.conventions, result.session_commands, result.options) + return SingleNodeBatchCommand(self._session.conventions, result.session_commands, result.options) def set_result(self, result: BatchCommandResult) -> None: def get_command_type(obj_node: dict) -> CommandType: @@ -62,23 +62,23 @@ def get_command_type(obj_node: dict) -> CommandType: type_as_str = str(c_type) - command_type = CommandType.from_csharp_value_str(type_as_str) - return command_type + _command_type = CommandType.from_csharp_value_str(type_as_str) + return _command_type if result.results is None: - self.__throw_on_null_result() + self._throw_on_null_result() return - self.__on_successful_request.clear_session_state_after_successful_save_changes() + self._on_successful_request.clear_session_state_after_successful_save_changes() - if self.__session.transaction_mode == TransactionMode.CLUSTER_WIDE: + if self._session.transaction_mode == TransactionMode.CLUSTER_WIDE: if result.transaction_index <= 0: raise ClientVersionMismatchException( "Cluster transaction was send to a node that is not supporting " - "it. So it was executed ONLY on the requested node on " + self.__session._request_executor.url + "it. So it was executed ONLY on the requested node on " + self._session.request_executor.url ) - for i in range(self.__session_commands_count): + for i in range(self._session_commands_count): batch_result = result.results[i] if batch_result is None: continue @@ -86,38 +86,38 @@ def get_command_type(obj_node: dict) -> CommandType: command_type = get_command_type(batch_result) if command_type == CommandType.PUT: - self.__handle_put(i, batch_result, False) + self._handle_put(i, batch_result, False) elif command_type == CommandType.FORCE_REVISION_CREATION: - self.__handle_force_revision_creation(batch_result) + self._handle_force_revision_creation(batch_result) elif command_type == CommandType.DELETE: - self.__handle_delete(batch_result) + self._handle_delete(batch_result) elif command_type == CommandType.COMPARE_EXCHANGE_PUT: - self.__handle_compare_exchange_put(batch_result) + self._handle_compare_exchange_put(batch_result) elif command_type == CommandType.COMPARE_EXCHANGE_DELETE: - self.__handle_compare_exchange_delete(batch_result) + self._handle_compare_exchange_delete(batch_result) else: raise ValueError(f"Command {command_type} is not supported") - for i in range(self.__session_commands_count, self.__all_commands_count): + for i in range(self._session_commands_count, self._all_commands_count): batch_result = result.results[i] if batch_result is None: continue command_type = get_command_type(batch_result) if command_type == CommandType.PUT: - self.__handle_put(i, batch_result, False) + self._handle_put(i, batch_result, False) elif command_type == CommandType.DELETE: - self.__handle_delete(batch_result) + self._handle_delete(batch_result) elif command_type == CommandType.PATCH: - self.__handle_patch(batch_result) + self._handle_patch(batch_result) elif command_type == CommandType.ATTACHMENT_PUT: - self.__handle_attachment_put(batch_result) + self._handle_attachment_put(batch_result) elif command_type == CommandType.ATTACHMENT_DELETE: - self.__handle_attachment_delete(batch_result) + self._handle_attachment_delete(batch_result) elif command_type == CommandType.ATTACHMENT_MOVE: - self.__handle_attachment_move(batch_result) + self._handle_attachment_move(batch_result) elif command_type == CommandType.ATTACHMENT_COPY: - self.__handle_attachment_copy(batch_result) + self._handle_attachment_copy(batch_result) elif command_type in ( CommandType.COMPARE_EXCHANGE_PUT, CommandType.COMPARE_EXCHANGE_DELETE, @@ -125,7 +125,7 @@ def get_command_type(obj_node: dict) -> CommandType: ): pass elif command_type == CommandType.COUNTERS: - self.__handle_counters(batch_result) + self._handle_counters(batch_result) elif command_type == CommandType.TIME_SERIES: break # todo: RavenDB-13474 add to time series cache elif command_type == CommandType.TIME_SERIES_COPY or command_type == CommandType.BATCH_PATCH: @@ -133,16 +133,16 @@ def get_command_type(obj_node: dict) -> CommandType: else: raise ValueError(f"Command {command_type} is not supported") - self.__finalize_result() + self._finalize_result() - def __finalize_result(self): - if not self.__modifications: + def _finalize_result(self): + if not self._modifications: return - for key, document_info in self.__modifications.items(): - self.__apply_metadata_modifications(key, document_info) + for key, document_info in self._modifications.items(): + self._apply_metadata_modifications(key, document_info) - def __apply_metadata_modifications(self, key: str, document_info: DocumentInfo): + def _apply_metadata_modifications(self, key: str, document_info: DocumentInfo): document_info.metadata_instance = None document_info.metadata = deepcopy(document_info.metadata) document_info.metadata[constants.Documents.Metadata.CHANGE_VECTOR] = document_info.change_vector @@ -151,43 +151,43 @@ def __apply_metadata_modifications(self, key: str, document_info: DocumentInfo): document_info.document = document_copy - def __get_or_add_modifications( + def _get_or_add_modifications( self, key: str, document_info: DocumentInfo, apply_modifications: bool ) -> DocumentInfo: - if not self.__modifications: - self.__modifications = CaseInsensitiveDict() + if not self._modifications: + self._modifications = CaseInsensitiveDict() - modified_document_info = self.__modifications.get(key) + modified_document_info = self._modifications.get(key) if modified_document_info is not None: if apply_modifications: - self.__apply_metadata_modifications(key, modified_document_info) + self._apply_metadata_modifications(key, modified_document_info) else: - self.__modifications[key] = modified_document_info = document_info + self._modifications[key] = modified_document_info = document_info return modified_document_info - def __handle_compare_exchange_put(self, batch_result: dict) -> None: - self.__handle_compare_exchange_internal(CommandType.COMPARE_EXCHANGE_PUT, batch_result) + def _handle_compare_exchange_put(self, batch_result: dict) -> None: + self._handle_compare_exchange_internal(CommandType.COMPARE_EXCHANGE_PUT, batch_result) - def __handle_compare_exchange_delete(self, batch_result: dict) -> None: - self.__handle_compare_exchange_internal(CommandType.COMPARE_EXCHANGE_DELETE, batch_result) + def _handle_compare_exchange_delete(self, batch_result: dict) -> None: + self._handle_compare_exchange_internal(CommandType.COMPARE_EXCHANGE_DELETE, batch_result) - def __handle_compare_exchange_internal(self, command_type: CommandType, batch_result: dict) -> None: + def _handle_compare_exchange_internal(self, command_type: CommandType, batch_result: dict) -> None: key: str = batch_result.get("Key") if not key: - self.__throw_missing_field(command_type, "Key") + self._throw_missing_field(command_type, "Key") index: int = batch_result.get("Index") if not index: - self.__throw_missing_field(command_type, "Index") + self._throw_missing_field(command_type, "Index") - cluster_session = self.__session.cluster_transaction + cluster_session = self._session.cluster_transaction cluster_session.update_state(key, index) - def __handle_patch(self, batch_result: dict) -> None: + def _handle_patch(self, batch_result: dict) -> None: patch_status = batch_result.get("PatchStatus") if not patch_status: - self.__throw_missing_field(CommandType.PATCH, "PatchStatus") + self._throw_missing_field(CommandType.PATCH, "PatchStatus") status = PatchStatus(patch_status) if status == PatchStatus.CREATED or PatchStatus.PATCHED: @@ -195,15 +195,15 @@ def __handle_patch(self, batch_result: dict) -> None: if not document: return - key = self.__get_string_field(batch_result, CommandType.PUT, "Id") - session_document_info = self.__session._documents_by_id.get(key) - if session_document_info == None: + key = self._get_string_field(batch_result, CommandType.PUT, "Id") + session_document_info = self._session.documents_by_id.get(key) + if session_document_info is None: return - document_info = self.__get_or_add_modifications(key, session_document_info, True) + document_info = self._get_or_add_modifications(key, session_document_info, True) - change_vector = self.__get_string_field(batch_result, CommandType.PATCH, "ChangeVector") - last_modified = self.__get_string_field(batch_result, CommandType.PATCH, "LastModified") + change_vector = self._get_string_field(batch_result, CommandType.PATCH, "ChangeVector") + last_modified = self._get_string_field(batch_result, CommandType.PATCH, "LastModified") document_info.change_vector = change_vector document_info.metadata[constants.Documents.Metadata.KEY] = key @@ -211,85 +211,90 @@ def __handle_patch(self, batch_result: dict) -> None: document_info.metadata[constants.Documents.Metadata.LAST_MODIFIED] = last_modified document_info.document = document - self.__apply_metadata_modifications(key, document_info) + self._apply_metadata_modifications(key, document_info) if document_info.entity is not None: - self.__session.entity_to_json.populate_entity(document_info.entity, key, document_info.document) - self.__session.after_save_changes_invoke( - AfterSaveChangesEventArgs(self.__session, document_info.key, document_info.entity) + self._session.entity_to_json.populate_entity(document_info.entity, key, document_info.document) + self._session.after_save_changes_invoke( + AfterSaveChangesEventArgs(self._session, document_info.key, document_info.entity) ) - def __handle_delete(self, batch_result: dict) -> None: - self.__handle_delete_internal(batch_result, CommandType.DELETE) + def _handle_delete(self, batch_result: dict) -> None: + self._handle_delete_internal(batch_result, CommandType.DELETE) - def __handle_delete_internal(self, batch_result: dict, command_type: CommandType): - key = self.__get_string_field(batch_result, command_type, "Id") - document_info = self.__session._documents_by_id.get(key) + def _handle_delete_internal(self, batch_result: dict, command_type: CommandType): + key = self._get_string_field(batch_result, command_type, "Id") + document_info = self._session.documents_by_id.get(key) if document_info is None: return - self.__session._documents_by_id.pop(key, None) + self._session.documents_by_id.pop(key, None) if document_info.entity is not None: - self.__session._documents_by_entity.pop(document_info.entity, None) - self.__session._deleted_entities.discard(document_info.entity) + self._session.documents_by_entity.pop(document_info.entity, None) + self._session.deleted_entities.discard(document_info.entity) + + def _handle_force_revision_creation(self, batch_result: dict) -> None: + # When forcing a revision for a document that does Not have any revisions yet then the HasRevisions flag is added to the document. + # In this case we need to update the tracked entities in the session with the document new change-vector. - def __handle_force_revision_creation(self, batch_result: dict) -> None: - if not self.__get_boolean_field(batch_result, CommandType.FORCE_REVISION_CREATION, "RevisionCreated"): + if not self._get_boolean_field(batch_result, CommandType.FORCE_REVISION_CREATION, "RevisionCreated"): # no forced revision was created...nothing to update return - id_ = self.__get_string_field( - batch_result, CommandType.FORCE_REVISION_CREATION, constants.Documents.Metadata.ID - ) - change_vector = self.__get_string_field( + id_ = self._get_string_field(batch_result, CommandType.FORCE_REVISION_CREATION, constants.Documents.Metadata.ID) + change_vector = self._get_string_field( batch_result, CommandType.FORCE_REVISION_CREATION, constants.Documents.Metadata.CHANGE_VECTOR ) - document_info = self.__session._documents_by_id.get(id_) - if not document_info: + document_info = self._session.documents_by_id.get(id_) + if document_info is None: return document_info.change_vector = change_vector - self.__handle_metadata_modifications(document_info, batch_result, id_, change_vector) - self.__session.__on_after_save_changes(self.__session, document_info.key, document_info.entity) + self._handle_metadata_modifications(document_info, batch_result, id_, change_vector) - def __handle_put(self, index: int, batch_result: dict, is_deferred: bool) -> None: + after_save_changes_event_args = AfterSaveChangesEventArgs( + self._session, document_info.key, document_info.entity + ) + self._session.after_save_changes_invoke(after_save_changes_event_args) + + def _handle_put(self, index: int, batch_result: dict, is_deferred: bool) -> None: entity = None document_info = None if not is_deferred: - entity = self.__entities[index] - document_info = self.__session._documents_by_entity.get(entity) + entity = self._entities[index] + document_info = self._session.documents_by_entity.get(entity) if document_info is None: return - key = self.__get_string_field(batch_result, CommandType.PUT, constants.Documents.Metadata.ID) - change_vector = self.__get_string_field( + key = self._get_string_field(batch_result, CommandType.PUT, constants.Documents.Metadata.ID) + change_vector = self._get_string_field( batch_result, CommandType.PUT, constants.Documents.Metadata.CHANGE_VECTOR ) if is_deferred: - session_document_info = self.__session._documents_by_id.get(key) + session_document_info = self._session.documents_by_id.get(key) if session_document_info is None: return - document_info = self.__get_or_add_modifications(key, session_document_info, True) + document_info = self._get_or_add_modifications(key, session_document_info, True) entity = document_info.entity - self.__handle_metadata_modifications(document_info, batch_result, key, change_vector) + self._handle_metadata_modifications(document_info, batch_result, key, change_vector) - self.__session._documents_by_id.update({document_info.key: document_info}) + self._session.documents_by_id.update({document_info.key: document_info}) if entity: - self.__session.generate_entity_id_on_the_client.try_set_identity(entity, key) + self._session.generate_entity_id_on_the_client.try_set_identity(entity, key) - self.__session.after_save_changes_invoke( - AfterSaveChangesEventArgs(self.__session, document_info.key, document_info.entity) + self._session.after_save_changes_invoke( + AfterSaveChangesEventArgs(self._session, document_info.key, document_info.entity) ) - def __handle_metadata_modifications( + def _handle_metadata_modifications( self, document_info: DocumentInfo, batch_result: dict, key: str, change_vector: str ) -> None: for property_name, value in batch_result.items(): @@ -301,26 +306,26 @@ def __handle_metadata_modifications( document_info.key = key document_info.change_vector = change_vector - self.__apply_metadata_modifications(key, document_info) + self._apply_metadata_modifications(key, document_info) - def __handle_counters(self, batch_result: dict) -> None: - doc_id = self.__get_string_field(batch_result, CommandType.COUNTERS, "Id") - counters_detail: dict = batch_result.get("CountersDetail", None) + def _handle_counters(self, batch_result: Dict) -> None: + doc_id = self._get_string_field(batch_result, CommandType.COUNTERS, "Id") + counters_detail: Optional[dict] = batch_result.get("CountersDetail", None) if counters_detail is None: - self.__throw_missing_field(CommandType.COUNTERS, "CountersDetail") + self._throw_missing_field(CommandType.COUNTERS, "CountersDetail") - counters = counters_detail.get("Counters", None) + counters: Optional[Dict] = counters_detail.get("Counters", None) if counters is None: - self.__throw_missing_field(CommandType.COUNTERS, "Counters") + self._throw_missing_field(CommandType.COUNTERS, "Counters") - cache = self.__session.counters_by_doc_id.get(doc_id, None) + cache = self._session.counters_by_doc_id.get(doc_id, None) if cache is None: cache = [False, CaseInsensitiveDict()] - self.__session.counters_by_doc_id[doc_id] = cache + self._session.counters_by_doc_id[doc_id] = cache - change_vector = self.__get_string_field(batch_result, CommandType.COUNTERS, "DocumentChangeVector", False) + change_vector = self._get_string_field(batch_result, CommandType.COUNTERS, "DocumentChangeVector", False) if change_vector is not None: - document_info = self.__session._documents_by_id.get(doc_id, None) + document_info = self._session.documents_by_id.get(doc_id, None) if document_info is not None: document_info.change_vector = change_vector @@ -331,30 +336,30 @@ def __handle_counters(self, batch_result: dict) -> None: if name is not None and value is not None: cache[1][name] = value - def __handle_attachment_put(self, batch_result: dict) -> None: - self.__handle_attachment_put_internal( + def _handle_attachment_put(self, batch_result: dict) -> None: + self._handle_attachment_put_internal( batch_result, CommandType.ATTACHMENT_PUT, "Id", "Name", "DocumentChangeVector" ) - def __handle_attachment_copy(self, batch_result: dict) -> None: - self.__handle_attachment_put_internal( + def _handle_attachment_copy(self, batch_result: dict) -> None: + self._handle_attachment_put_internal( batch_result, CommandType.ATTACHMENT_COPY, "Id", "Name", "DocumentChangeVector" ) - def __handle_attachment_move(self, batch_result: dict) -> None: - self.__handle_attachment_delete_internal( + def _handle_attachment_move(self, batch_result: dict) -> None: + self._handle_attachment_delete_internal( batch_result, CommandType.ATTACHMENT_MOVE, "Id", "Name", "DocumentChangeVector" ) - self.__handle_attachment_put_internal( + self._handle_attachment_put_internal( batch_result, CommandType.ATTACHMENT_MOVE, "DestinationId", "DestinationName", "DocumentChangeVector" ) - def __handle_attachment_delete(self, batch_result: dict) -> None: - self.__handle_attachment_delete_internal( + def _handle_attachment_delete(self, batch_result: dict) -> None: + self._handle_attachment_delete_internal( batch_result, CommandType.ATTACHMENT_DELETE, constants.Documents.Metadata.ID, "Name", "DocumentChangeVector" ) - def __handle_attachment_delete_internal( + def _handle_attachment_delete_internal( self, batch_result: dict, command_type: CommandType, @@ -362,15 +367,15 @@ def __handle_attachment_delete_internal( attachment_name_field_name: str, document_change_vector_field_name: str, ) -> None: - key = self.__get_string_field(batch_result, command_type, id_field_name) + key = self._get_string_field(batch_result, command_type, id_field_name) - session_document_info = self.__session._documents_by_id.get_value(key) + session_document_info = self._session.documents_by_id.get_value(key) if session_document_info is None: return - document_info = self.__get_or_add_modifications(key, session_document_info, True) + document_info = self._get_or_add_modifications(key, session_document_info, True) - document_change_vector = self.__get_string_field( + document_change_vector = self._get_string_field( batch_result, command_type, document_change_vector_field_name, False ) if document_change_vector: @@ -380,19 +385,19 @@ def __handle_attachment_delete_internal( if not attachments_json: return - name = self.__get_string_field(batch_result, command_type, attachment_name_field_name) + name = self._get_string_field(batch_result, command_type, attachment_name_field_name) attachments = [] document_info.metadata[constants.Documents.Metadata.ATTACHMENTS] = attachments for attachment in attachments_json: - attachment_name = self.__get_string_field(attachment, command_type, "Name") + attachment_name = self._get_string_field(attachment, command_type, "Name") if attachment_name == name: continue attachments.append(attachment) - def __handle_attachment_put_internal( + def _handle_attachment_put_internal( self, batch_result: dict, command_type: CommandType, @@ -400,14 +405,14 @@ def __handle_attachment_put_internal( attachment_name_field_name: str, document_change_vector_field_name: str, ) -> None: - key = self.__get_string_field(batch_result, command_type, id_field_name) + key = self._get_string_field(batch_result, command_type, id_field_name) - session_document_info = self.__session._documents_by_id.get_value(key) + session_document_info = self._session.documents_by_id.get_value(key) if session_document_info is None: return - document_info = self.__get_or_add_modifications(key, session_document_info, False) - document_change_vector = self.__get_string_field( + document_info = self._get_or_add_modifications(key, session_document_info, False) + document_change_vector = self._get_string_field( batch_result, command_type, document_change_vector_field_name, False ) if document_change_vector: @@ -419,38 +424,38 @@ def __handle_attachment_put_internal( document_info.metadata[constants.Documents.Metadata.ATTACHMENTS] = attachments dynamic_node = { - "ChangeVector": self.__get_string_field(batch_result, command_type, "ChangeVector"), - "ContentType": self.__get_string_field(batch_result, command_type, "ContentType"), - "Hash": self.__get_string_field(batch_result, command_type, "Hash"), - "Name": self.__get_string_field(batch_result, command_type, "Name"), - "Size": self.__get_string_field(batch_result, command_type, "Size"), + "ChangeVector": self._get_string_field(batch_result, command_type, "ChangeVector"), + "ContentType": self._get_string_field(batch_result, command_type, "ContentType"), + "Hash": self._get_string_field(batch_result, command_type, "Hash"), + "Name": self._get_string_field(batch_result, command_type, "Name"), + "Size": self._get_string_field(batch_result, command_type, "Size"), } attachments.append(dynamic_node) - def __get_string_field( + def _get_string_field( self, json: dict, command_type: CommandType, field_name: str, throw_on_missing: Optional[bool] = True ) -> str: json_node = json.get(field_name, None) if throw_on_missing and json_node is None: - self.__throw_missing_field(command_type, field_name) + self._throw_missing_field(command_type, field_name) return str(json_node) - def __get_int_field(self, json: dict, command_type: CommandType, field_name: str) -> int: + def _get_int_field(self, json: dict, command_type: CommandType, field_name: str) -> int: json_node = json.get(field_name) - if (not json_node) or not isinstance(json_node, int): - self.__throw_missing_field(command_type, field_name) + if not isinstance(json_node, int): + self._throw_missing_field(command_type, field_name) return json_node - def __get_boolean_field(self, json: dict, command_type: CommandType, field_name: str) -> bool: - json_node = json.get(field_name) - if (not json_node) or not isinstance(json_node, bool): - self.__throw_missing_field(command_type, field_name) + def _get_boolean_field(self, json: dict, command_type: CommandType, field_name: str) -> bool: + json_node = json.get(field_name, None) + if not isinstance(json_node, bool): + self._throw_missing_field(command_type, field_name) return json_node - def __throw_on_null_result(self) -> None: + def _throw_on_null_result(self) -> None: raise ValueError( "Reveived empty response from the server. This is not supposed to happend and is likely a bug." ) - def __throw_missing_field(self, c_type: CommandType, field_name: str) -> None: + def _throw_missing_field(self, c_type: CommandType, field_name: str) -> None: raise ValueError(f"{c_type} response is invalid. Field '{field_name}' is missing.") diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py index 0bdb9fb8..7944d9e8 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py @@ -93,3 +93,70 @@ def test_force_revision_creation_for_tracked_entity_with_changes_by_entity(self) # Assert revision contains the value 'Before' the changes... # ('Before' is the default force revision creation strategy) self.assertEqual("HR", revisions[0].name) + + def test_force_revision_creation_across_multiple_sessions(self): + company_id = "" + + with self.store.open_session() as session: + company = Company() + company.name = "HR" + + session.store(company) + session.save_changes() + + company_id = company.Id + revisions_count = len(session.advanced.revisions.get_for(company.Id, Company)) + self.assertEqual(0, revisions_count) + + session.advanced.revisions.force_revision_creation_for(company) + session.save_changes() + + revisions_count = len(session.advanced.revisions.get_for(company.Id, Company)) + self.assertEqual(1, revisions_count) + + # Verify that another 'force' request will not create another revision + session.advanced.revisions.force_revision_creation_for(company) + session.save_changes() + + revisions_count = len(session.advanced.revisions.get_for(company.Id, Company)) + self.assertEqual(1, revisions_count) + + with self.store.open_session() as session: + company = session.load(company_id, Company) + company.name = "HR V2" + + session.advanced.revisions.force_revision_creation_for(company) + session.save_changes() + + revisions = session.advanced.revisions.get_for(company.Id, Company) + revisions_count = len(revisions) + + self.assertEqual(1, revisions_count) + # Assert revision contains the value 'Before' the changes... + self.assertEqual("HR", revisions[0].name) + + session.advanced.revisions.force_revision_creation_for(company) + session.save_changes() + + revisions = session.advanced.revisions.get_for(company.Id, Company) + revisions_count = len(revisions) + + self.assertEqual(2, revisions_count) + + # Assert revision contains the value 'Before' the changes... + self.assertEqual("HR V2", revisions[0].name) + + with self.store.open_session() as session: + company = session.load(company_id, Company) + company.name = "HR V3" + session.save_changes() + + with self.store.open_session() as session: + session.advanced.revisions.force_revision_creation_for_id(company_id) + session.save_changes() + + revisions = session.advanced.revisions.get_for(company_id, Company) + revisions_count = len(revisions) + + self.assertEqual(3, revisions_count) + self.assertEqual("HR V3", revisions[0].name) From e1dc248e33a060e0b1ca2866c2fee7a5be7d01ab Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 4 Jan 2024 14:32:03 +0100 Subject: [PATCH 07/33] RDBC-775 ForceRevisionCreationTestForceRevisionCreationTest::forceRevisionCreationMultipleRequests --- .../test_force_revision_creation.py | 45 ++++++++++++++++--- ravendb/tests/test_base.py | 9 ++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py index 7944d9e8..d5e1a907 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py @@ -1,3 +1,4 @@ +from ravendb.documents.session.misc import ForceRevisionStrategy from ravendb.infrastructure.orders import Company from ravendb.tests.test_base import TestBase @@ -7,9 +8,6 @@ def setUp(self): super().setUp() def test_has_revisions_flag_is_created_when_forcing_revision_for_document_that_has_no_revisions_yet(self): - company1id = "" - company2id = "" - with self.store.open_session() as session: company1 = Company() company1.name = "HR1" @@ -63,8 +61,6 @@ def test_has_revisions_flag_is_created_when_forcing_revision_for_document_that_h self.assertEqual("HasRevisions", metadata["@flags"]) def test_force_revision_creation_for_tracked_entity_with_changes_by_entity(self): - company_id = "" - # 1. Store document with self.store.open_session() as session: company = Company() @@ -95,8 +91,6 @@ def test_force_revision_creation_for_tracked_entity_with_changes_by_entity(self) self.assertEqual("HR", revisions[0].name) def test_force_revision_creation_across_multiple_sessions(self): - company_id = "" - with self.store.open_session() as session: company = Company() company.name = "HR" @@ -160,3 +154,40 @@ def test_force_revision_creation_across_multiple_sessions(self): self.assertEqual(3, revisions_count) self.assertEqual("HR V3", revisions[0].name) + + def test_force_revision_creation_multiple_requests(self): + with self.store.open_session() as session: + company = Company() + company.name = "HR" + session.store(company) + session.save_changes() + + company_id = company.Id + + revisions_count = len(session.advanced.revisions.get_for(company.Id, Company)) + self.assertEqual(0, revisions_count) + + with self.store.open_session() as session: + session.advanced.revisions.force_revision_creation_for_id(company_id) + + company = session.load(company_id, Company) + company.name = "HR V2" + + session.advanced.revisions.force_revision_creation_for(company) + # The above request should not throw - we ignore duplicate requests with SAME strategy + + self.assertRaisesWithMessageContaining( + session.advanced.revisions.force_revision_creation_for_id, + RuntimeError, + "A request for creating a revision was already made for document", + company.Id, + ForceRevisionStrategy.NONE, + ) + + session.save_changes() + + revisions = session.advanced.revisions.get_for(company.Id, Company) + revisions_count = len(revisions) + + self.assertEqual(1, revisions_count) + self.assertEqual("HR", revisions[0].name) diff --git a/ravendb/tests/test_base.py b/ravendb/tests/test_base.py index 7546b114..b901f781 100644 --- a/ravendb/tests/test_base.py +++ b/ravendb/tests/test_base.py @@ -441,6 +441,15 @@ def assertRaisesWithMessage(self, func, exception, msg, *args, **kwargs): self.assertIsNotNone(e) self.assertEqual(msg, e.args[0]) + def assertRaisesWithMessageContaining(self, func, exception, msg, *args, **kwargs): + e = None + try: + func(*args, **kwargs) + except exception as ex: + e = ex + self.assertIsNotNone(e) + self.assertIn(msg, e.args[0]) + def assertSequenceContainsElements(self, sequence, *args): for arg in args: self.assertIn(arg, sequence) From aa002c877f2f6f28536dfca03e3070753b000e64 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 4 Jan 2024 15:05:28 +0100 Subject: [PATCH 08/33] RDBC-775 ForceRevisionCreationTest::forceRevisionCreationForTrackedEntityWithChangesByID --- .../test_force_revision_creation.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py index d5e1a907..62788d7b 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py @@ -191,3 +191,32 @@ def test_force_revision_creation_multiple_requests(self): self.assertEqual(1, revisions_count) self.assertEqual("HR", revisions[0].name) + + def test_force_revision_creation_for_tracked_entity_with_changes_by_ID(self): + # 1. Store document + with self.store.open_session() as session: + company = Company() + company.name = "HR" + session.store(company) + session.save_changes() + + company_id = company.Id + + revisions_count = len(session.advanced.revisions.get_for(company.Id, Company)) + self.assertEqual(0, revisions_count) + + with self.store.open_session() as session: + # 2. Load, Make changes & Save + company = session.load(company_id, Company) + company.name = "HR V2" + + session.advanced.revisions.force_revision_creation_for(company.Id) + session.save_changes() + + revisions = session.advanced.revisions.get_for(company.Id, Company) + revisions_count = len(revisions) + + self.assertEqual(1, revisions_count) + + # Assert revision contains the value 'Before' the changes... + self.assertEqual("HR", revisions[0].name) From b0935748e401e561154c4dc5fbe44ef4e0f88902 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 4 Jan 2024 15:07:21 +0100 Subject: [PATCH 09/33] RDBC-775 ForceRevisionCreationTest::forceRevisionCreationForSingleUnTrackedEntityByID --- .../test_force_revision_creation.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py index 62788d7b..31abc906 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py @@ -220,3 +220,19 @@ def test_force_revision_creation_for_tracked_entity_with_changes_by_ID(self): # Assert revision contains the value 'Before' the changes... self.assertEqual("HR", revisions[0].name) + + def test_force_revision_creation_for_single_untracked_entity_by_ID(self): + with self.store.open_session() as session: + company = Company() + company.name = "HR" + session.store(company) + + company_id = company.Id + session.save_changes() + + with self.store.open_session() as session: + session.advanced.revisions.force_revision_creation_for_id(company_id) + session.save_changes() + + revisions_count = len(session.advanced.revisions.get_for(company_id, Company)) + self.assertEqual(1, revisions_count) From 1d6081a1115283472f6dcc866a48f652e0a327fe Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 4 Jan 2024 15:39:09 +0100 Subject: [PATCH 10/33] RDBC-775 ForceRevisionCreationTest::forceRevisionCreationWhenRevisionConfigurationIsSet --- ravendb/documents/operations/revisions.py | 2 +- ravendb/http/request_executor.py | 74 +++++++++---------- .../test_force_revision_creation.py | 55 ++++++++++++++ 3 files changed, 93 insertions(+), 38 deletions(-) diff --git a/ravendb/documents/operations/revisions.py b/ravendb/documents/operations/revisions.py index f2cac822..714644ed 100644 --- a/ravendb/documents/operations/revisions.py +++ b/ravendb/documents/operations/revisions.py @@ -68,7 +68,7 @@ def __init__( def to_json(self) -> Dict[str, Any]: return { - "Default": self.default_config.to_json(), + "Default": self.default_config.to_json() if self.default_config else None, "Collections": {key: value.to_json() for key, value in self.collections.items()} if self.collections else None, diff --git a/ravendb/http/request_executor.py b/ravendb/http/request_executor.py index 458b2e5a..20268041 100644 --- a/ravendb/http/request_executor.py +++ b/ravendb/http/request_executor.py @@ -64,7 +64,7 @@ def __init__( self.conventions = copy(conventions) self._node_selector: NodeSelector = None self.__default_timeout: datetime.timedelta = conventions.request_timeout - self.__cache: HttpCache = HttpCache() + self._cache: HttpCache = HttpCache() self.__certificate_path = certificate_path self.__trust_store_path = trust_store_path @@ -75,9 +75,9 @@ def __init__( self.__failed_nodes_timers: Dict[ServerNode, NodeStatus] = {} - self.__database_name = database_name + self._database_name = database_name - self.__last_returned_response: Union[None, datetime.datetime] = None + self._last_returned_response: Union[None, datetime.datetime] = None self._thread_pool_executor = ( ThreadPoolExecutor(max_workers=10) if not thread_pool_executor else thread_pool_executor @@ -107,7 +107,7 @@ def __init__( self.__synchronized_lock = Lock() # --- events --- - self.__on_before_request: List[Callable[[BeforeRequestEventArgs], Any]] = [] + self._on_before_request: List[Callable[[BeforeRequestEventArgs], Any]] = [] self.__on_failed_request: List[Callable[[FailedRequestEventArgs], None]] = [] self.__on_succeed_request: List[Callable[[SucceedRequestEventArgs], None]] = [] self._on_topology_updated: List[Callable[[Topology], None]] = [] @@ -123,7 +123,7 @@ def close(self): return self._disposed = True - self.__cache.close() + self._cache.close() if self.__update_topology_timer: self.__update_topology_timer.cancel() @@ -177,7 +177,7 @@ def __create_http_session(self) -> requests.Session: @property def cache(self) -> HttpCache: - return self.__cache + return self._cache @property def topology_nodes(self) -> List[ServerNode]: @@ -215,15 +215,15 @@ def get_requested_node(self, node_tag: str, throw_if_contains_failures: bool = F def __on_failed_request_invoke(self, url: str, e: Exception): for event in self.__on_failed_request: - event(FailedRequestEventArgs(self.__database_name, url, e, None, None)) + event(FailedRequestEventArgs(self._database_name, url, e, None, None)) def __on_failed_request_invoke_details( self, url: str, e: Exception, request: Optional[requests.Request], response: Optional[requests.Response] ) -> None: for event in self.__on_failed_request: - event(FailedRequestEventArgs(self.__database_name, url, e, request, response)) + event(FailedRequestEventArgs(self._database_name, url, e, request, response)) - def __on_succeed_request_invoke( + def _on_succeed_request_invoke( self, database: str, url: str, response: requests.Response, request: requests.Request, attempt_number: int ): for event in self.__on_succeed_request: @@ -246,10 +246,10 @@ def remove_on_failed_request(self, event: Callable[[FailedRequestEventArgs], Non self.__on_failed_request.remove(event) def add_on_before_request(self, event: Callable[[BeforeRequestEventArgs], None]): - self.__on_before_request.append(event) + self._on_before_request.append(event) def remove_on_before_request(self, event: Callable[[BeforeRequestEventArgs], None]): - self.__on_before_request.remove(event) + self._on_before_request.remove(event) def add_on_topology_updated(self, event: Callable[[Topology], None]): self._on_topology_updated.append(event) @@ -404,7 +404,7 @@ def _first_topology_update(self, input_urls: List[str], application_identifier: def __run(errors: list): for url in initial_urls: try: - server_node = ServerNode(url, self.__database_name) + server_node = ServerNode(url, self._database_name) update_parameters = UpdateTopologyParameters(server_node) update_parameters.timeout_in_ms = 0x7FFFFFFF update_parameters.debug_tag = "first-topology-update" @@ -430,7 +430,7 @@ def __run(errors: list): self._topology_etag, self.topology_nodes if self.topology_nodes - else list(map(lambda url_val: ServerNode(url_val, self.__database_name, "!"), initial_urls)), + else list(map(lambda url_val: ServerNode(url_val, self._database_name, "!"), initial_urls)), ) self._node_selector = NodeSelector(topology, self._thread_pool_executor) @@ -508,38 +508,38 @@ def execute( no_caching = session_info.no_caching if session_info else False - cached_item, change_vector, cached_value = self.__get_from_cache(command, not no_caching, url) + cached_item, change_vector, cached_value = self._get_from_cache(command, not no_caching, url) # todo: if change_vector exists try get from cache - aggressive caching with cached_item: # todo: try get from cache - self.__set_request_headers(session_info, change_vector, request) + self._set_request_headers(session_info, change_vector, request) command.number_of_attempts = command.number_of_attempts + 1 attempt_num = command.number_of_attempts - for func in self.__on_before_request: - func(BeforeRequestEventArgs(self.__database_name, url, request, attempt_num)) - response = self.__send_request_to_server( + for func in self._on_before_request: + func(BeforeRequestEventArgs(self._database_name, url, request, attempt_num)) + response = self._send_request_to_server( chosen_node, node_index, command, should_retry, session_info, request, url ) if response is None: return - refresh_tasks = self.__refresh_if_needed(chosen_node, response) + refresh_tasks = self._refresh_if_needed(chosen_node, response) command.status_code = response.status_code response_dispose = ResponseDisposeHandling.AUTOMATIC try: if response.status_code == HTTPStatus.NOT_MODIFIED: - self.__on_succeed_request_invoke(self.__database_name, url, response, request, attempt_num) + self._on_succeed_request_invoke(self._database_name, url, response, request, attempt_num) cached_item.not_modified() if command.response_type == RavenCommandResponseType.OBJECT: command.set_response(cached_value, True) return if response.status_code >= 400: - if not self.__handle_unsuccessful_response( + if not self._handle_unsuccessful_response( chosen_node, node_index, command, @@ -552,11 +552,11 @@ def execute( db_missing_header = response.headers.get("Database-Missing", None) if db_missing_header is not None: raise DatabaseDoesNotExistException(db_missing_header) - self.__throw_failed_to_contact_all_nodes(command, request) + self._throw_failed_to_contact_all_nodes(command, request) return # we either handled this already in the unsuccessful response or we are throwing - self.__on_succeed_request_invoke(self.__database_name, url, response, request, attempt_num) - response_dispose = command.process_response(self.__cache, response, url) - self.__last_returned_response = datetime.datetime.utcnow() + self._on_succeed_request_invoke(self._database_name, url, response, request, attempt_num) + response_dispose = command.process_response(self._cache, response, url) + self._last_returned_response = datetime.datetime.utcnow() finally: if response_dispose == ResponseDisposeHandling.AUTOMATIC: response.close() @@ -566,7 +566,7 @@ def execute( except: raise - def __refresh_if_needed(self, chosen_node: ServerNode, response: requests.Response) -> List[Future]: + def _refresh_if_needed(self, chosen_node: ServerNode, response: requests.Response) -> List[Future]: refresh_topology = response.headers.get(constants.Headers.REFRESH_TOPOLOGY, False) refresh_client_configuration = response.headers.get(constants.Headers.REFRESH_CLIENT_CONFIGURATION, False) @@ -587,7 +587,7 @@ def __refresh_if_needed(self, chosen_node: ServerNode, response: requests.Respon return [refresh_task, refresh_client_configuration_task] - def __send_request_to_server( + def _send_request_to_server( self, chosen_node: ServerNode, node_index: int, @@ -617,7 +617,7 @@ def __send_request_to_server( if not self.__handle_server_down( url, chosen_node, node_index, command, request, None, t, session_info, should_retry ): - self.__throw_failed_to_contact_all_nodes(command, request) + self._throw_failed_to_contact_all_nodes(command, request) return None except IOError as e: @@ -627,7 +627,7 @@ def __send_request_to_server( if not self.__handle_server_down( url, chosen_node, node_index, command, request, None, e, session_info, should_retry ): - self.__throw_failed_to_contact_all_nodes(command, request) + self._throw_failed_to_contact_all_nodes(command, request) return None @@ -722,7 +722,7 @@ def __wait_for_topology_update(self, topology_update: Future[None]) -> None: def __update_topology_callback(self) -> None: time = datetime.datetime.now() - if (time - self.__last_returned_response.time) <= datetime.timedelta(minutes=5): + if (time - self._last_returned_response.time) <= datetime.timedelta(minutes=5): return try: @@ -744,7 +744,7 @@ def __update_topology_callback(self) -> None: except Exception as e: self.logger.info("Couldn't update topology from __update_topology_timer", exc_info=e) - def __set_request_headers( + def _set_request_headers( self, session_info: SessionInfo, cached_change_vector: Union[None, str], request: requests.Request ) -> None: if cached_change_vector is not None: @@ -764,7 +764,7 @@ def __set_request_headers( if not request.headers.get(constants.Headers.CLIENT_VERSION): request.headers[constants.Headers.CLIENT_VERSION] = RequestExecutor.CLIENT_VERSION - def __get_from_cache( + def _get_from_cache( self, command: RavenCommand, use_cache: bool, url: str ) -> Tuple[HttpCache.ReleaseCacheItem, Optional[str], Optional[str]]: if ( @@ -773,7 +773,7 @@ def __get_from_cache( and command.is_read_request and command.response_type == RavenCommandResponseType.OBJECT ): - return self.__cache.get(url) + return self._cache.get(url) return HttpCache.ReleaseCacheItem(), None, None @@ -784,7 +784,7 @@ def __try_get_server_version(response: requests.Response) -> Union[None, str]: if server_version_header is not None: return server_version_header - def __throw_failed_to_contact_all_nodes(self, command: RavenCommand, request: requests.Request): + def _throw_failed_to_contact_all_nodes(self, command: RavenCommand, request: requests.Request): if not command.failed_nodes: raise RuntimeError( "Received unsuccessful response and couldn't recover from it. " @@ -856,7 +856,7 @@ def __supply_async( ) -> (RequestExecutor.IndexAndResponse, int): try: request, str_ref = self.__create_request(nodes[task_number], command) - self.__set_request_headers(None, None, request) + self._set_request_headers(None, None, request) return self.IndexAndResponse(task_number, command.send(self.http_session, request)) except Exception as e: single_element_list_number_failed_tasks[0] += 1 @@ -979,7 +979,7 @@ def __wait_for_broadcast_result(self, command: RavenCommand, tasks: Dict[Future[ raise AllTopologyNodesDownException(f"Broadcasting {command.__class__.__name__} failed: {exceptions}") - def __handle_unsuccessful_response( + def _handle_unsuccessful_response( self, chosen_node: ServerNode, node_index: int, @@ -991,7 +991,7 @@ def __handle_unsuccessful_response( should_retry: bool, ) -> bool: if response.status_code == HTTPStatus.NOT_FOUND: - self.__cache.set_not_found(url, False) # todo : check if aggressively cached, don't just pass False + self._cache.set_not_found(url, False) # todo : check if aggressively cached, don't just pass False if command.response_type == RavenCommandResponseType.EMPTY: return True elif command.response_type == RavenCommandResponseType.OBJECT: diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py index 31abc906..f8c494b7 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py @@ -1,3 +1,5 @@ +from ravendb import RevisionsConfiguration, RevisionsCollectionConfiguration +from ravendb.documents.operations.revisions import ConfigureRevisionsOperation from ravendb.documents.session.misc import ForceRevisionStrategy from ravendb.infrastructure.orders import Company from ravendb.tests.test_base import TestBase @@ -236,3 +238,56 @@ def test_force_revision_creation_for_single_untracked_entity_by_ID(self): revisions_count = len(session.advanced.revisions.get_for(company_id, Company)) self.assertEqual(1, revisions_count) + + def test_force_revision_creation_when_revision_configuration_is_set(self): + # Define revisions settings + configuration = RevisionsConfiguration() + + companies_configuration = RevisionsCollectionConfiguration() + companies_configuration.purge_on_delete = True + companies_configuration.minimum_revisions_to_keep = 5 + + configuration.collections = {"Companies": companies_configuration} + + result = self.store.maintenance.send(ConfigureRevisionsOperation(configuration)) + + with self.store.open_session() as session: + company = Company() + company.name = "HR" + session.store(company) + company_id = company.Id + session.save_changes() + + revisions_count = len(session.advanced.revisions.get_for(company.Id, Company)) + self.assertEqual(1, revisions_count) # one revision because configuration is set + + session.advanced.revisions.force_revision_creation_for(company) + session.save_changes() + + revisions_count = len(session.advanced.revisions.get_for(company.Id, Company)) + self.assertEqual( + 1, revisions_count + ) # no new revision created - already exists due to configuration settings + + session.advanced.revisions.force_revision_creation_for(company) + session.save_changes() + + company.name = "HR V2" + session.save_changes() + + revisions = session.advanced.revisions.get_for(company_id, Company) + revisions_count = len(revisions) + + self.assertEqual(2, revisions_count) + self.assertEqual("HR V2", revisions[0].name) + + with self.store.open_session() as session: + session.advanced.revisions.force_revision_creation_for_id(company_id) + session.save_changes() + + revisions = session.advanced.revisions.get_for(company_id, Company) + revisions_count = len(revisions) + + self.assertEqual(2, revisions_count) + + self.assertEqual("HR V2", revisions[0].name) From 62c1ac457c16ad6c00088d7f2dc7bf1aa33f8d59 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 4 Jan 2024 15:43:09 +0100 Subject: [PATCH 11/33] RDBC-775 ForceRevisionCreationTest::forceRevisionCreationForNewDocumentByEntity --- .../test_force_revision_creation.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py index f8c494b7..a5cc2a47 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py @@ -291,3 +291,20 @@ def test_force_revision_creation_when_revision_configuration_is_set(self): self.assertEqual(2, revisions_count) self.assertEqual("HR V2", revisions[0].name) + + def test_force_revision_creation_for_new_document_by_entity(self): + with self.store.open_session() as session: + company = Company() + company.name = "HR" + session.store(company) + session.save_changes() + + session.advanced.revisions.force_revision_creation_for(company) + + revisions_count = len(session.advanced.revisions.get_for(company.Id, Company)) + self.assertEqual(0, revisions_count) + + session.save_changes() + + revisions_count = len(session.advanced.revisions.get_for(company.Id, Company)) + self.assertEqual(1, revisions_count) From 27bb94e50a93ebde1175f128d13f2c7ac32fd838 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 4 Jan 2024 15:45:27 +0100 Subject: [PATCH 12/33] RDBC-775 ForceRevisionCreationTest::cannotForceRevisionCreationForUnTrackedEntityByEntity --- .../revisions_tests/test_force_revision_creation.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py index a5cc2a47..328938d2 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py @@ -308,3 +308,15 @@ def test_force_revision_creation_for_new_document_by_entity(self): revisions_count = len(session.advanced.revisions.get_for(company.Id, Company)) self.assertEqual(1, revisions_count) + + def test_cannot_force_revision_creation_for_untracked_entity_by_entity(self): + with self.store.open_session() as session: + company = Company() + company.name = "HR" + + self.assertRaisesWithMessage( + session.advanced.revisions.force_revision_creation_for, + RuntimeError, + "Cannot create a revision for the requested entity because it is not tracked by the session", + company, + ) From e3267b14abbd392f7cb2a4416f41139d9899d304 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 4 Jan 2024 15:48:26 +0100 Subject: [PATCH 13/33] RDBC-775 ForceRevisionCreationTest::forceRevisionCreationForMultipleUnTrackedEntitiesByID --- .../test_force_revision_creation.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py index 328938d2..7705b016 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py @@ -320,3 +320,31 @@ def test_cannot_force_revision_creation_for_untracked_entity_by_entity(self): "Cannot create a revision for the requested entity because it is not tracked by the session", company, ) + + def test_force_revision_creation_for_multiple_untracked_entities_by_ID(self): + with self.store.open_session() as session: + company1 = Company() + company1.name = "HR1" + + company2 = Company() + company2.name = "HR2" + + session.store(company1) + session.store(company2) + + companyid1 = company1.Id + companyid2 = company2.Id + + session.save_changes() + + with self.store.open_session() as session: + session.advanced.revisions.force_revision_creation_for_id(companyid1) + session.advanced.revisions.force_revision_creation_for_id(companyid2) + + session.save_changes() + + revisions_count_1 = len(session.advanced.revisions.get_for(companyid1, Company)) + revisions_count_2 = len(session.advanced.revisions.get_for(companyid2, Company)) + + self.assertEqual(1, revisions_count_1) + self.assertEqual(1, revisions_count_2) From 18586ad87e57f76e5b615960703476e70046d833 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 4 Jan 2024 16:15:40 +0100 Subject: [PATCH 14/33] RDBC-775 ForceRevisionCreationTest::forceRevisionCreationForTrackedEntityWithNoChangesByEntity --- .../test_force_revision_creation.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py index 7705b016..94b3e503 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py @@ -348,3 +348,28 @@ def test_force_revision_creation_for_multiple_untracked_entities_by_ID(self): self.assertEqual(1, revisions_count_1) self.assertEqual(1, revisions_count_2) + + def test_force_revision_creation_for_tracked_entity_with_no_changes_by_entity(self): + company_id = "" + + with self.store.open_session() as session: + # 1. store document + company = Company() + company.name = "HR" + session.store(company) + session.save_changes() + + company_id = company.Id + + revisions_count = len(session.advanced.revisions.get_for(company.Id, Company)) + self.assertEqual(0, revisions_count) + + with self.store.open_session() as session: + # 2. Load & Save without making changes to the document + company = session.load(company_id, Company) + + session.advanced.revisions.force_revision_creation_for(company) + session.save_changes() + + revisions_count = len(session.advanced.revisions.get_for(company_id, Company)) + self.assertEqual(1, revisions_count) From 333ed50b537eec42f2012ab02744c24664f76c02 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 4 Jan 2024 16:34:45 +0100 Subject: [PATCH 15/33] RDBC-775 ForceRevisionCreationTest::cannotForceRevisionCreationForNewDocumentBeforeSavingToServerByEntity --- .../test_force_revision_creation.py | 20 ++++++++++++++++++- ravendb/tests/test_base.py | 8 ++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py index 94b3e503..d7d5f184 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_force_revision_creation.py @@ -1,6 +1,7 @@ from ravendb import RevisionsConfiguration, RevisionsCollectionConfiguration from ravendb.documents.operations.revisions import ConfigureRevisionsOperation from ravendb.documents.session.misc import ForceRevisionStrategy +from ravendb.exceptions.raven_exceptions import RavenException from ravendb.infrastructure.orders import Company from ravendb.tests.test_base import TestBase @@ -212,7 +213,7 @@ def test_force_revision_creation_for_tracked_entity_with_changes_by_ID(self): company = session.load(company_id, Company) company.name = "HR V2" - session.advanced.revisions.force_revision_creation_for(company.Id) + session.advanced.revisions.force_revision_creation_for_id(company.Id) session.save_changes() revisions = session.advanced.revisions.get_for(company.Id, Company) @@ -373,3 +374,20 @@ def test_force_revision_creation_for_tracked_entity_with_no_changes_by_entity(se revisions_count = len(session.advanced.revisions.get_for(company_id, Company)) self.assertEqual(1, revisions_count) + + def test_cannot_force_revision_creation_for_new_document_before_saving_to_server_by_entity(self): + with self.store.open_session() as session: + company = Company() + company.name = "HR" + session.store(company) + + session.advanced.revisions.force_revision_creation_for(company) + + self.assertRaisesWithMessageContaining( + session.save_changes, + RuntimeError, + "Can't force revision creation - the document was not saved on the server yet", + ) + + revisions_count = len(session.advanced.revisions.get_for(company.Id, Company)) + self.assertEqual(0, revisions_count) diff --git a/ravendb/tests/test_base.py b/ravendb/tests/test_base.py index b901f781..3114f702 100644 --- a/ravendb/tests/test_base.py +++ b/ravendb/tests/test_base.py @@ -436,7 +436,9 @@ def assertRaisesWithMessage(self, func, exception, msg, *args, **kwargs): e = None try: func(*args, **kwargs) - except exception as ex: + except Exception as ex: + if not isinstance(ex, exception): + raise AssertionError(f"Expected exception '{exception}' but got '{type(ex)}'.") e = ex self.assertIsNotNone(e) self.assertEqual(msg, e.args[0]) @@ -445,7 +447,9 @@ def assertRaisesWithMessageContaining(self, func, exception, msg, *args, **kwarg e = None try: func(*args, **kwargs) - except exception as ex: + except Exception as ex: + if not isinstance(ex, exception): + raise AssertionError(f"Expected exception '{exception}' but got '{type(ex)}'.") e = ex self.assertIsNotNone(e) self.assertIn(msg, e.args[0]) From d39da630d6a90db1df29d78e612df71b41ff3e21 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Fri, 5 Jan 2024 10:53:02 +0100 Subject: [PATCH 16/33] RDBC-775 RevisionsTest::canGetRevisionsByChangeVectors --- .../session/operations/operations.py | 1 + .../revisions_tests/test_revisions.py | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/ravendb/documents/session/operations/operations.py b/ravendb/documents/session/operations/operations.py index b217ceb2..efca5d60 100644 --- a/ravendb/documents/session/operations/operations.py +++ b/ravendb/documents/session/operations/operations.py @@ -170,6 +170,7 @@ def from_change_vectors( ) -> GetRevisionOperation: self = cls(session) self._command = GetRevisionsCommand.from_change_vectors(change_vectors) + return self def create_request(self) -> GetRevisionsCommand: if self._command.change_vectors is not None: diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py index 983d27e2..37baf28d 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py @@ -1,6 +1,7 @@ from time import sleep from ravendb.infrastructure.entities import User +from ravendb.infrastructure.orders import Company from ravendb.primitives import constants from ravendb.tests.test_base import TestBase @@ -52,3 +53,30 @@ def test_revisions(self): metadata_skip_first[0].metadata.get(constants.Documents.Metadata.CHANGE_VECTOR), User ) self.assertEqual("user3", user.name) + + def test_can_get_revisions_by_change_vector(self): + id_ = "users/1" + self.setup_revisions(self.store, False, 100) + + with self.store.open_session() as session: + user = User() + user.name = "Fitzchak" + session.store(user, id_) + session.save_changes() + + for i in range(10): + with self.store.open_session() as session: + user = session.load(id_, Company) + user.name = f"Fitzchak{i}" + session.save_changes() + + with self.store.open_session() as session: + revisions_metadata = session.advanced.revisions.get_metadata_for(id_) + self.assertEqual(11, len(revisions_metadata)) + + change_vectors = [x[constants.Documents.Metadata.CHANGE_VECTOR] for x in revisions_metadata] + change_vectors.append("NotExistsChangeVector") + + revisions = session.advanced.revisions.get_by_change_vectors(change_vectors, User) + self.assertIsNone(revisions.get("NotExistsChangeVector")) + self.assertIsNone(session.advanced.revisions.get_by_change_vector("NotExistsChangeVector", User)) From d524eb7d347d6dd7bbf9f240ec8073a1cf9af317 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Fri, 5 Jan 2024 10:57:31 +0100 Subject: [PATCH 17/33] RDBC-775 RevisionsTest::canGetRevisionsCountFor --- .../revisions_tests/test_revisions.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py index 37baf28d..13da4ce3 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py @@ -80,3 +80,26 @@ def test_can_get_revisions_by_change_vector(self): revisions = session.advanced.revisions.get_by_change_vectors(change_vectors, User) self.assertIsNone(revisions.get("NotExistsChangeVector")) self.assertIsNone(session.advanced.revisions.get_by_change_vector("NotExistsChangeVector", User)) + + def test_can_get_revisions_count_for(self): + company = Company() + company.name = "Company Name" + + self.setup_revisions(self.store, False, 100) + with self.store.open_session() as session: + session.store(company) + session.save_changes() + + with self.store.open_session() as session: + company2 = session.load(company.Id, Company) + company2.fax = "Israel" + session.save_changes() + + with self.store.open_session() as session: + company3 = session.load(company.Id, Company) + company3.name = "Hibernating Rhinos" + session.save_changes() + + with self.store.open_session() as session: + companies_revisions_count = session.advanced.revisions.get_count_for(company.Id) + self.assertEqual(3, companies_revisions_count) From 1d401f6b6af66cbd620ef1231c6c18c69b2b728d Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Fri, 5 Jan 2024 11:43:48 +0100 Subject: [PATCH 18/33] RDBC-775 RevisionsTest::canListRevisionsBin + GetRevisionsBinEntryCommand (V6.0 - like !!!) --- ravendb/documents/commands/revisions.py | 27 +++++++++++++++++++ .../revisions_tests/test_revisions.py | 22 +++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/ravendb/documents/commands/revisions.py b/ravendb/documents/commands/revisions.py index 965f7d5f..34c62b97 100644 --- a/ravendb/documents/commands/revisions.py +++ b/ravendb/documents/commands/revisions.py @@ -114,3 +114,30 @@ def set_response(self, response: Optional[str], from_cache: bool) -> None: def is_read_request(self) -> bool: return True + + +class GetRevisionsBinEntryCommand(RavenCommand[JsonArrayResult]): + def __init__(self, etag: int, page_size: int): + super().__init__(JsonArrayResult) + self._etag = etag + self._page_size = page_size + + def create_request(self, node: ServerNode) -> requests.Request: + request = requests.Request("GET") + path = [node.url, "/databases/", node.database, "/revisions/bin?start=", str(self._etag)] + if self._page_size is not None: + path.append("&pageSize=") + path.append(str(self._page_size)) + + request.url = "".join(path) + + return request + + def set_response(self, response: Optional[str], from_cache: bool) -> None: + if response is None: + self._throw_invalid_response() + + self.result = JsonArrayResult.from_json(json.loads(response)) + + def is_read_request(self) -> bool: + return True diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py index 13da4ce3..6daacad2 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py @@ -1,8 +1,10 @@ from time import sleep +from ravendb.documents.commands.revisions import GetRevisionsBinEntryCommand from ravendb.infrastructure.entities import User from ravendb.infrastructure.orders import Company from ravendb.primitives import constants +from ravendb.primitives.constants import int_max from ravendb.tests.test_base import TestBase @@ -103,3 +105,23 @@ def test_can_get_revisions_count_for(self): with self.store.open_session() as session: companies_revisions_count = session.advanced.revisions.get_count_for(company.Id) self.assertEqual(3, companies_revisions_count) + + def test_can_list_revisions_bin(self): + self.setup_revisions(self.store, False, 4) + + with self.store.open_session() as session: + user = User(name="user1") + session.store(user, "users/1") + session.save_changes() + + with self.store.open_session() as session: + session.delete("users/1") + session.save_changes() + + revisions_bin_entry_command = GetRevisionsBinEntryCommand(0, 20) + self.store.get_request_executor().execute_command(revisions_bin_entry_command) + + result = revisions_bin_entry_command.result + self.assertEqual(1, len(result.results)) + + self.assertEqual("users/1", result.results[0].get("@metadata").get("@id")) From 8037e45c9c9af9d42e3d5ca3411ebb0689506803 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Fri, 5 Jan 2024 12:29:01 +0100 Subject: [PATCH 19/33] RDBC-775 RevisionsTest::collectionCaseSensitiveTest1 --- ravendb/documents/operations/revisions.py | 2 +- .../revisions_tests/test_revisions.py | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/ravendb/documents/operations/revisions.py b/ravendb/documents/operations/revisions.py index 714644ed..6f69aa66 100644 --- a/ravendb/documents/operations/revisions.py +++ b/ravendb/documents/operations/revisions.py @@ -28,7 +28,7 @@ def __init__( minimum_revisions_to_keep: int = None, minimum_revisions_age_to_keep: timedelta = None, disabled: bool = False, - purge_on_delete: bool = None, + purge_on_delete: bool = False, maximum_revisions_to_delete_upon_document_creation: int = None, ): self.minimum_revisions_to_keep = minimum_revisions_to_keep diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py index 6daacad2..8282d4df 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py @@ -1,6 +1,8 @@ from time import sleep +from ravendb import RevisionsConfiguration, RevisionsCollectionConfiguration from ravendb.documents.commands.revisions import GetRevisionsBinEntryCommand +from ravendb.documents.operations.revisions import ConfigureRevisionsOperation from ravendb.infrastructure.entities import User from ravendb.infrastructure.orders import Company from ravendb.primitives import constants @@ -125,3 +127,29 @@ def test_can_list_revisions_bin(self): self.assertEqual(1, len(result.results)) self.assertEqual("users/1", result.results[0].get("@metadata").get("@id")) + + def test_collection_case_sensitive_test_1(self): + id_ = "user/1" + configuration = RevisionsConfiguration() + + collection_configuration = RevisionsCollectionConfiguration() + collection_configuration.disabled = False + + configuration.collections = {"uSErs": collection_configuration} + + self.store.maintenance.send(ConfigureRevisionsOperation(configuration)) + + with self.store.open_session() as session: + user = User(name="raven") + session.store(user, id_) + session.save_changes() + + for i in range(10): + with self.store.open_session() as session: + user = session.load(id_, Company) + user.name = "raven" + str(i) + session.save_changes() + + with self.store.open_session() as session: + revisions_metadata = session.advanced.revisions.get_metadata_for(id_) + self.assertEqual(11, len(revisions_metadata)) From 5ebac866f05fdd229409400a0ada59a81da92d30 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Fri, 5 Jan 2024 12:31:03 +0100 Subject: [PATCH 20/33] RDBC-775 RevisionsTest::collectionCaseSensitiveTest2 --- .../revisions_tests/test_revisions.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py index 8282d4df..61ab9f2d 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py @@ -153,3 +153,28 @@ def test_collection_case_sensitive_test_1(self): with self.store.open_session() as session: revisions_metadata = session.advanced.revisions.get_metadata_for(id_) self.assertEqual(11, len(revisions_metadata)) + + def test_collection_case_sensitive_test_2(self): + id_ = "uSEr/1" + configuration = RevisionsConfiguration() + + collection_configuration = RevisionsCollectionConfiguration() + collection_configuration.disabled = False + + configuration.collections = {"users": collection_configuration} + self.store.maintenance.send(ConfigureRevisionsOperation(configuration)) + + with self.store.open_session() as session: + user = User(name="raven") + session.store(user, id_) + session.save_changes() + + for i in range(10): + with self.store.open_session() as session: + user = session.load(id_, Company) + user.name = "raven" + str(i) + session.save_changes() + + with self.store.open_session() as session: + revisions_metadata = session.advanced.revisions.get_metadata_for(id_) + self.assertEqual(11, len(revisions_metadata)) From 634ac45e20453d7cf28185f2676c7d12a2e8325b Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Fri, 5 Jan 2024 12:40:51 +0100 Subject: [PATCH 21/33] RDBC-775 RevisionsTest::collectionCaseSensitiveTest3 --- .../client_tests/revisions_tests/test_revisions.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py index 61ab9f2d..800dcaae 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py @@ -178,3 +178,15 @@ def test_collection_case_sensitive_test_2(self): with self.store.open_session() as session: revisions_metadata = session.advanced.revisions.get_metadata_for(id_) self.assertEqual(11, len(revisions_metadata)) + + def test_collection_case_sensitive_test_3(self): + configuration = RevisionsConfiguration() + c1 = RevisionsCollectionConfiguration() + c1.disabled = False + + c2 = RevisionsCollectionConfiguration() + c2.disabled = False + + configuration.collections = {"users": c1, "USERS": c2} + with self.assertRaises(RuntimeError): + self.store.maintenance.send(ConfigureRevisionsOperation(configuration)) From 982d6c21800435905ede19f6f1181ce59d386083 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Fri, 5 Jan 2024 13:05:42 +0100 Subject: [PATCH 22/33] RDBC-775 RevisionsTest::canGetAllRevisionsForDocument_UsingStoreOperation --- ravendb/documents/operations/revisions.py | 6 +++-- .../revisions_tests/test_revisions.py | 24 ++++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/ravendb/documents/operations/revisions.py b/ravendb/documents/operations/revisions.py index 6f69aa66..1ac7f076 100644 --- a/ravendb/documents/operations/revisions.py +++ b/ravendb/documents/operations/revisions.py @@ -12,11 +12,13 @@ from ravendb.http.raven_command import RavenCommand from ravendb.util.util import RaftIdGenerator from ravendb.http.topology import RaftCommand +from ravendb.documents.session.entity_to_json import EntityToJson +from ravendb.documents.conventions import DocumentConventions + if TYPE_CHECKING: from ravendb.http.http_cache import HttpCache - from ravendb import DocumentStore, ServerNode, EntityToJson - from ravendb.documents.conventions import DocumentConventions + from ravendb import DocumentStore, ServerNode _T = TypeVar("_T") diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py index 800dcaae..132b2e13 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py @@ -2,7 +2,8 @@ from ravendb import RevisionsConfiguration, RevisionsCollectionConfiguration from ravendb.documents.commands.revisions import GetRevisionsBinEntryCommand -from ravendb.documents.operations.revisions import ConfigureRevisionsOperation +from ravendb.documents.operations.revisions import ConfigureRevisionsOperation, GetRevisionsOperation +from ravendb.documents.session.operations.operations import GetRevisionOperation from ravendb.infrastructure.entities import User from ravendb.infrastructure.orders import Company from ravendb.primitives import constants @@ -190,3 +191,24 @@ def test_collection_case_sensitive_test_3(self): configuration.collections = {"users": c1, "USERS": c2} with self.assertRaises(RuntimeError): self.store.maintenance.send(ConfigureRevisionsOperation(configuration)) + + def test_can_get_all_revisions_for_document_using_store_operation(self): + company = Company(name="Company Name") + self.setup_revisions(self.store, False, 123) + + with self.store.open_session() as session: + session.store(company) + session.save_changes() + + with self.store.open_session() as session: + company3 = session.load(company.Id, Company) + company3.name = "Hibernating Rhinos" + session.save_changes() + + revisions_result = self.store.operations.send(GetRevisionsOperation(company.Id, Company)) + self.assertEqual(2, revisions_result.total_results) + + companies_revisions = revisions_result.results + self.assertEqual(2, len(companies_revisions)) + self.assertEqual("Hibernating Rhinos", companies_revisions[0].name) + self.assertEqual("Company Name", companies_revisions[1].name) From bbd62d3ffb030ed18353a8746c44979bf127f475 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Fri, 5 Jan 2024 13:10:04 +0100 Subject: [PATCH 23/33] RDBC-775 RevisionsTest::canGetRevisionsWithPaging2_UsingStoreOperation --- .../revisions_tests/test_revisions.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py index 132b2e13..8f1b09eb 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py @@ -212,3 +212,29 @@ def test_can_get_all_revisions_for_document_using_store_operation(self): self.assertEqual(2, len(companies_revisions)) self.assertEqual("Hibernating Rhinos", companies_revisions[0].name) self.assertEqual("Company Name", companies_revisions[1].name) + + def test_can_get_revisions_with_paging2_using_store_operation(self): + self.setup_revisions(self.store, False, 100) + id_ = "companies/1" + + with self.store.open_session() as session: + session.store(Company(), id_) + session.save_changes() + + for i in range(99): + with self.store.open_session() as session: + company = session.load(id_, Company) + company.name = "HR" + str(i) + session.save_changes() + + revisions_result = self.store.operations.send(GetRevisionsOperation(id_, Company, 50, 10)) + + self.assertEqual(100, revisions_result.total_results) + + companies_revisions = revisions_result.results + self.assertEqual(10, len(companies_revisions)) + + count = 0 + for i in range(48, 38, -1): + self.assertEqual("HR" + str(i), companies_revisions[count].name) + count += 1 From b9b5f235f747bb8de2475af9f04bb29e51d8066a Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Fri, 5 Jan 2024 13:15:20 +0100 Subject: [PATCH 24/33] RDBC-775 RevisionsTest::canGetRevisionsWithPaging_UsingStoreOperation --- ravendb/documents/operations/revisions.py | 3 +- .../revisions_tests/test_revisions.py | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/ravendb/documents/operations/revisions.py b/ravendb/documents/operations/revisions.py index 1ac7f076..3dd1b513 100644 --- a/ravendb/documents/operations/revisions.py +++ b/ravendb/documents/operations/revisions.py @@ -123,7 +123,7 @@ def __init__( self._parameters = parameters @classmethod - def from_parameters(cls, parameters: Parameters, object_type: Type[_T] = None): + def from_parameters(cls, parameters: Parameters, object_type: Type[_T] = None) -> GetRevisionsOperation[_T]: if parameters is None: raise ValueError("Parameters cannot be None") @@ -132,6 +132,7 @@ def from_parameters(cls, parameters: Parameters, object_type: Type[_T] = None): operation = cls() operation._object_type = object_type operation._parameters = parameters + return operation def get_command(self, store: DocumentStore, conventions: DocumentConventions, cache: HttpCache) -> RavenCommand[_T]: return self.GetRevisionsResultCommand( diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py index 8f1b09eb..8f6f53a0 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py @@ -213,6 +213,45 @@ def test_can_get_all_revisions_for_document_using_store_operation(self): self.assertEqual("Hibernating Rhinos", companies_revisions[0].name) self.assertEqual("Company Name", companies_revisions[1].name) + def test_can_get_revisions_with_paging_using_store_operation(self): + self.setup_revisions(self.store, False, 123) + + id_ = "companies/1" + + with self.store.open_session() as session: + session.store(Company(), id_) + session.save_changes() + + with self.store.open_session() as session: + company2 = session.load(id_, Company) + company2.name = "Hibernating" + session.save_changes() + + with self.store.open_session() as session: + company3 = session.load(id_, Company) + company3.name = "Hibernating Rhinos" + session.save_changes() + + for i in range(10): + with self.store.open_session() as session: + company = session.load(id_, Company) + company.name = f"HR{i}" + session.save_changes() + + parameters = GetRevisionsOperation.Parameters() + parameters.id_ = id_ + parameters.start = 10 + revisions_result = self.store.operations.send(GetRevisionsOperation.from_parameters(parameters, Company)) + + self.assertEqual(13, revisions_result.total_results) + + companies_revisions = revisions_result.results + self.assertEqual(3, len(companies_revisions)) + + self.assertEqual("Hibernating Rhinos", companies_revisions[0].name) + self.assertEqual("Hibernating", companies_revisions[1].name) + self.assertIsNone(companies_revisions[2].name) + def test_can_get_revisions_with_paging2_using_store_operation(self): self.setup_revisions(self.store, False, 100) id_ = "companies/1" From 8a40c1998d2ca3aab0864bff8bc8b77a280ac0b0 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Fri, 5 Jan 2024 15:04:15 +0100 Subject: [PATCH 25/33] RDBC-775 RevisionsTest::canGetMetadataForLazily --- ravendb/__init__.py | 2 +- .../lazy/{lazy_operation.py => definition.py} | 2 +- .../documents/operations/lazy/revisions.py | 117 ++++++++++++++++++ ravendb/documents/session/document_session.py | 4 +- .../in_memory_document_session_operations.py | 2 +- .../session/document_session_revisions.py | 11 +- ravendb/documents/session/operations/lazy.py | 20 +-- .../session/operations/operations.py | 2 +- ravendb/json/result.py | 2 +- .../revisions_tests/test_revisions.py | 35 ++++++ 10 files changed, 177 insertions(+), 20 deletions(-) rename ravendb/documents/operations/lazy/{lazy_operation.py => definition.py} (94%) create mode 100644 ravendb/documents/operations/lazy/revisions.py diff --git a/ravendb/__init__.py b/ravendb/__init__.py index 443d92d2..60a4b423 100644 --- a/ravendb/__init__.py +++ b/ravendb/__init__.py @@ -103,7 +103,7 @@ GetIndexErrorsOperation, IndexingStatus, ) -from ravendb.documents.operations.lazy.lazy_operation import LazyOperation +from ravendb.documents.operations.lazy.definition import LazyOperation from ravendb.documents.operations.misc import DeleteByQueryOperation, GetOperationStateOperation, QueryOperationOptions from ravendb.documents.operations.patch import ( PatchOperation, diff --git a/ravendb/documents/operations/lazy/lazy_operation.py b/ravendb/documents/operations/lazy/definition.py similarity index 94% rename from ravendb/documents/operations/lazy/lazy_operation.py rename to ravendb/documents/operations/lazy/definition.py index e010c123..ba3cef26 100644 --- a/ravendb/documents/operations/lazy/lazy_operation.py +++ b/ravendb/documents/operations/lazy/definition.py @@ -16,7 +16,7 @@ def __init__(self): @property @abstractmethod - def is_requires_retry(self) -> bool: + def requires_retry(self) -> bool: pass @abstractmethod diff --git a/ravendb/documents/operations/lazy/revisions.py b/ravendb/documents/operations/lazy/revisions.py new file mode 100644 index 00000000..598ff8a3 --- /dev/null +++ b/ravendb/documents/operations/lazy/revisions.py @@ -0,0 +1,117 @@ +import datetime +import json +from enum import Enum +from typing import TYPE_CHECKING, Type, TypeVar, List, Optional, Dict + +from ravendb.documents.commands.multi_get import GetRequest, GetResponse +from ravendb.documents.operations.lazy.definition import LazyOperation +from ravendb.documents.session.operations.operations import GetRevisionOperation +from ravendb.json.result import JsonArrayResult +from ravendb.json.metadata_as_dictionary import MetadataAsDictionary + +if TYPE_CHECKING: + from ravendb.documents.session.document_session import DocumentSession + from ravendb.documents.queries.query import QueryResult + from ravendb.documents.store.lazy import Lazy + + +_T = TypeVar("_T") + + +class LazyRevisionOperations: + def __init__(self, delegate: "DocumentSession"): + self._delegate = delegate + + def get_by_change_vector(self, change_vector: str, object_type: Type[_T]) -> "Lazy[List[_T]]": + operation = GetRevisionOperation.from_change_vector(self._delegate, change_vector) + lazy_revision_operation = LazyRevisionOperation(operation, LazyRevisionOperation.Mode.SINGLE, object_type) + return self._delegate.add_lazy_operation(object_type, lazy_revision_operation, None) + + def get_metadata_for( + self, id_: str, start: Optional[int] = 0, page_size: Optional[int] = 25 + ) -> "Lazy[List[MetadataAsDictionary]]": + operation = GetRevisionOperation.from_start_page(self._delegate, id_, start, page_size) + lazy_revision_operation = LazyRevisionOperation( + operation, LazyRevisionOperation.Mode.LIST_OF_METADATA, MetadataAsDictionary + ) + return self._delegate.add_lazy_operation(list, lazy_revision_operation, None) + + def get_by_change_vectors( + self, change_vectors: List[str], object_type: Optional[Type[_T]] = None + ) -> "Lazy[Dict[str, _T]]": + operation = GetRevisionOperation.from_change_vectors(self._delegate, change_vectors) + lazy_revision_operation = LazyRevisionOperation(operation, LazyRevisionOperation.Mode.MAP, object_type) + return self._delegate.add_lazy_operation(dict, lazy_revision_operation, None) + + def get_by_before_date( + self, id_: str, date: datetime.datetime, object_type: Optional[Type[_T]] = None + ) -> "Lazy[_T]": + operation = GetRevisionOperation.from_before_date(self._delegate, id_, date) + lazy_revision_operation = LazyRevisionOperation(operation, LazyRevisionOperation.Mode.SINGLE, object_type) + return self._delegate.add_lazy_operation(object_type, lazy_revision_operation, None) + + def get_for( + self, id_: str, object_type: Optional[Type[_T]] = None, start: Optional[int] = 0, page_size: Optional[int] = 25 + ) -> "Lazy[List[_T]]": + operation = GetRevisionOperation.from_start_page(self._delegate, id_, start, page_size) + lazy_revision_operation = LazyRevisionOperation(operation, LazyRevisionOperation.Mode.MULTI, object_type) + return self._delegate.add_lazy_operation(list, lazy_revision_operation, None) + + +class LazyRevisionOperation(LazyOperation): + class Mode(Enum): + SINGLE = "Single" + MULTI = "Multi" + MAP = "Map" + LIST_OF_METADATA = "ListOfMetadata" + + def __init__( + self, get_revision_operation: GetRevisionOperation, mode: Mode, object_type: Optional[Type[_T]] = None + ): + self.result: Optional[object] = None + self.query_result: Optional[QueryResult] = None + self._requires_retry: Optional[bool] = None + + self._object_type = object_type + self._get_revision_operation = get_revision_operation + self._mode = mode + + @property + def requires_retry(self) -> Optional[bool]: + return self._requires_retry + + @requires_retry.setter + def requires_retry(self, value: bool): + self._requires_retry = value + + def create_request(self) -> Optional[GetRequest]: + get_revisions_command = self._get_revision_operation.command + sb = ["?"] + get_revisions_command.get_request_query_string(sb) + get_request = GetRequest() + get_request.method = "GET" + get_request.url = "/revisions" + get_request.query = "".join(sb) + return get_request + + def handle_response(self, response: GetResponse) -> None: + if response.result is None: + return + + response_as_dict = json.loads(response.result) + json_array = response_as_dict.get("Results") + + json_array_result = JsonArrayResult() + json_array_result.results = json_array + self._get_revision_operation.set_result(json_array_result) + + if self._mode == LazyRevisionOperation.Mode.SINGLE: + self.result = self._get_revision_operation.get_revision(self._object_type) + elif self._mode == LazyRevisionOperation.Mode.MULTI: + self.result = self._get_revision_operation.get_revisions_for(self._object_type) + elif self._mode == LazyRevisionOperation.Mode.MAP: + self.result = self._get_revision_operation.get_revisions(self._object_type) + elif self._mode == LazyRevisionOperation.Mode.LIST_OF_METADATA: + self.result = self._get_revision_operation.get_revisions_metadata_for() + else: + raise ValueError(f"Invalid mode: {self._mode}") diff --git a/ravendb/documents/session/document_session.py b/ravendb/documents/session/document_session.py index 642f1d64..92e74479 100644 --- a/ravendb/documents/session/document_session.py +++ b/ravendb/documents/session/document_session.py @@ -99,7 +99,7 @@ if TYPE_CHECKING: from ravendb.documents.conventions import DocumentConventions - from ravendb.documents.operations.lazy.lazy_operation import LazyOperation + from ravendb.documents.operations.lazy.definition import LazyOperation from ravendb.http.request_executor import RequestExecutor from ravendb.documents.store.definition import DocumentStore @@ -229,7 +229,7 @@ def _execute_lazy_operations_single_step( ) self._pending_lazy_operations[i].handle_response(response) - if self._pending_lazy_operations[i].is_requires_retry: + if self._pending_lazy_operations[i].requires_retry: return True return False 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 55148143..fe636144 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 @@ -57,7 +57,7 @@ from ravendb.documents.store.misc import IdTypeAndName if TYPE_CHECKING: - from ravendb.documents.operations.lazy.lazy_operation import LazyOperation + from ravendb.documents.operations.lazy.definition import LazyOperation from ravendb.documents.store.definition import DocumentStore from ravendb.http.request_executor import RequestExecutor diff --git a/ravendb/documents/session/document_session_revisions.py b/ravendb/documents/session/document_session_revisions.py index 62c02554..a8a7e11a 100644 --- a/ravendb/documents/session/document_session_revisions.py +++ b/ravendb/documents/session/document_session_revisions.py @@ -1,6 +1,7 @@ import datetime from typing import TYPE_CHECKING, TypeVar, Type, List, Dict +from ravendb.documents.operations.lazy.revisions import LazyRevisionOperations from ravendb.documents.session.misc import ForceRevisionStrategy from ravendb.documents.session.operations.operations import GetRevisionOperation, GetRevisionsCountOperation @@ -10,6 +11,8 @@ ) from ravendb.documents.commands.batches import CommandData from ravendb.json.metadata_as_dictionary import MetadataAsDictionary + from ravendb.documents.session.document_session import DocumentSession + _T = TypeVar("_T") @@ -73,8 +76,10 @@ class DocumentSessionRevisions(DocumentSessionRevisionsBase): def __init__(self, session: "InMemoryDocumentSessionOperations"): super().__init__(session) - # def lazily(self) -> LazyRevisionOperations: - # return LazyRevisionOperations(self.session) + @property + def lazily(self) -> LazyRevisionOperations: + self.session: "DocumentSession" + return LazyRevisionOperations(self.session) def get_for(self, id_: str, object_type: Type[_T] = None, start: int = 0, page_size: int = 25) -> List[_T]: operation = GetRevisionOperation.from_start_page(self.session, id_, start, page_size) @@ -130,7 +135,7 @@ def get_by_change_vectors(self, change_vectors: List[str], object_type: Type[_T] return operation.get_revisions(object_type) def get_by_before_date(self, id_: str, before_date: datetime.datetime, object_type: Type[_T] = None) -> _T: - operation = GetRevisionOperation.from_before(self.session, id_, before_date) + operation = GetRevisionOperation.from_before_date(self.session, id_, before_date) command = operation.create_request() if command is None: return operation.get_revision(object_type) diff --git a/ravendb/documents/session/operations/lazy.py b/ravendb/documents/session/operations/lazy.py index b0024366..5f90208e 100644 --- a/ravendb/documents/session/operations/lazy.py +++ b/ravendb/documents/session/operations/lazy.py @@ -23,7 +23,7 @@ from ravendb.documents.commands.crud import GetDocumentsResult, ConditionalGetResult from ravendb.documents.session.operations.load_operation import LoadOperation from ravendb.documents.conventions import DocumentConventions -from ravendb.documents.operations.lazy.lazy_operation import LazyOperation +from ravendb.documents.operations.lazy.definition import LazyOperation from ravendb.documents.commands.multi_get import Content, GetRequest, GetResponse if TYPE_CHECKING: @@ -74,7 +74,7 @@ def __init__( self.requires_retry: Union[None, bool] = None @property - def is_requires_retry(self) -> bool: + def requires_retry(self) -> bool: return self.requires_retry def create_request(self) -> GetRequest: @@ -136,7 +136,7 @@ def query_result(self) -> QueryResult: raise NotImplementedError() @property - def is_requires_retry(self) -> bool: + def requires_retry(self) -> bool: return self.__requires_retry def create_request(self) -> GetRequest: @@ -285,7 +285,7 @@ def result(self, value) -> None: self.__result = value @property - def is_requires_retry(self) -> bool: + def requires_retry(self) -> bool: return self.__requires_retry def create_request(self) -> Optional[GetRequest]: @@ -344,7 +344,7 @@ def __handle_response(self, load_result: GetDocumentsResult) -> None: LoadOperation(self.__session).by_keys(self.__already_in_session).get_documents(self.__object_type) self.__load_operation.set_result(load_result) - if not self.is_requires_retry: + if not self.requires_retry: self.result = self.__load_operation.get_documents(self.__object_type) @@ -394,7 +394,7 @@ def query_result(self, value: QueryResult): self.__query_result = value @property - def is_requires_retry(self) -> bool: + def requires_retry(self) -> bool: return self.__requires_retry def handle_response(self, response: "GetResponse") -> None: @@ -460,7 +460,7 @@ def __handle_response(self, query_result: QueryResult) -> None: self.query_result = query_result @property - def is_requires_retry(self) -> bool: + def requires_retry(self) -> bool: return self.__requires_retry @@ -506,7 +506,7 @@ def __handle_response(self, query_result: QueryResult) -> None: self.query_result = query_result @property - def is_requires_retry(self) -> bool: + def requires_retry(self) -> bool: return self.__requires_retry @@ -542,7 +542,7 @@ def query_result(self) -> QueryResult: raise NotImplementedError("Not implemented") @property - def is_requires_retry(self) -> bool: + def requires_retry(self) -> bool: return self.__requires_retry def create_request(self) -> Optional["GetRequest"]: @@ -636,7 +636,7 @@ def query_result(self) -> QueryResult: raise NotImplementedError("Not implemented.") @property - def is_requires_retry(self) -> bool: + def requires_retry(self) -> bool: return self.__requires_retry def create_request(self) -> Optional["GetRequest"]: diff --git a/ravendb/documents/session/operations/operations.py b/ravendb/documents/session/operations/operations.py index efca5d60..6ce9fd42 100644 --- a/ravendb/documents/session/operations/operations.py +++ b/ravendb/documents/session/operations/operations.py @@ -151,7 +151,7 @@ def from_start_page( return self @classmethod - def from_before( + def from_before_date( cls, session: InMemoryDocumentSessionOperations, id_: str, before: datetime ) -> GetRevisionOperation: self = cls(session) diff --git a/ravendb/json/result.py b/ravendb/json/result.py index af3888fb..f82015ee 100644 --- a/ravendb/json/result.py +++ b/ravendb/json/result.py @@ -8,7 +8,7 @@ def __init__(self, results, transaction_index): class JsonArrayResult: - def __init__(self, results: List): + def __init__(self, results: List = None): self.results = results @classmethod diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py index 8f6f53a0..44196850 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py @@ -277,3 +277,38 @@ def test_can_get_revisions_with_paging2_using_store_operation(self): for i in range(48, 38, -1): self.assertEqual("HR" + str(i), companies_revisions[count].name) count += 1 + + def test_can_get_metadata_for_lazily(self): + id_ = "users/1" + id_2 = "users/2" + + self.setup_revisions(self.store, False, 100) + + with self.store.open_session() as session: + user1 = User() + user1.name = "Omer" + session.store(user1, id_) + + user2 = User() + user2.name = "Rhinos" + session.store(user2, id_2) + + session.save_changes() + + for i in range(10): + with self.store.open_session() as session: + user = session.load(id_, Company) + user.name = f"Omer{i}" + session.save_changes() + + with self.store.open_session() as session: + revisions_metadata = session.advanced.revisions.get_metadata_for(id_) + revisions_metadata_lazily = session.advanced.revisions.lazily.get_metadata_for(id_) + revisions_metadata_lazily_2 = session.advanced.revisions.lazily.get_metadata_for(id_2) + revisions_metadata_lazily_result = revisions_metadata_lazily.value + + ids = [x["@id"] for x in revisions_metadata] + ids_lazily = [x["@id"] for x in revisions_metadata_lazily_result] + + self.assertEqual(ids, ids_lazily) + self.assertEqual(2, session.advanced.number_of_requests) From 3114095044a9eb3e597a871e5eb8b0977cb05481 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Fri, 5 Jan 2024 15:18:05 +0100 Subject: [PATCH 26/33] RDBC-775 small fix --- ravendb/documents/session/operations/lazy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ravendb/documents/session/operations/lazy.py b/ravendb/documents/session/operations/lazy.py index 5f90208e..70f46c8d 100644 --- a/ravendb/documents/session/operations/lazy.py +++ b/ravendb/documents/session/operations/lazy.py @@ -71,11 +71,11 @@ def __init__( self.result = None self.query_result: Union[None, QueryResult] = None - self.requires_retry: Union[None, bool] = None + self._requires_retry: Union[None, bool] = None @property def requires_retry(self) -> bool: - return self.requires_retry + return self._requires_retry def create_request(self) -> GetRequest: request = GetRequest() From 995a65c6f810a6971df084e0d66783d8ffde7234 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Fri, 5 Jan 2024 15:25:47 +0100 Subject: [PATCH 27/33] RDBC-775 RevisionsTest::canGetForLazily --- .../revisions_tests/test_revisions.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py index 44196850..9ab7e78d 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py @@ -312,3 +312,41 @@ def test_can_get_metadata_for_lazily(self): self.assertEqual(ids, ids_lazily) self.assertEqual(2, session.advanced.number_of_requests) + + def test_can_get_for_lazily(self): + id_ = "users/1" + id_2 = "users/2" + + self.setup_revisions(self.store, False, 100) + + with self.store.open_session() as session: + user1 = User() + user1.name = "Omer" + session.store(user1, id_) + + user2 = User() + user2.name = "Rhinos" + session.store(user2, id_2) + + session.save_changes() + + for i in range(10): + with self.store.open_session() as session: + user = session.load(id_, Company) + user.name = f"Omer{i}" + session.save_changes() + + with self.store.open_session() as session: + revision = session.advanced.revisions.get_for("users/1", User) + revisions_lazily = session.advanced.revisions.lazily.get_for("users/1", User) + session.advanced.revisions.lazily.get_for("users/2", User) + + revisions_lazily_result = revisions_lazily.value + + names = [x.name for x in revision] + names_lazily = [x.name for x in revisions_lazily_result] + self.assertEqual(names, names_lazily) + + ids = [x.Id for x in revision] + ids_lazily = [x.Id for x in revisions_lazily_result] + self.assertEqual(ids, ids_lazily) From af7fb7336232343cf36634ee5fb4ca44528f0af7 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Fri, 5 Jan 2024 15:32:13 +0100 Subject: [PATCH 28/33] RDBC-775 RevisionsTest::canGetRevisionsByIdAndTimeLazily --- ravendb/documents/commands/revisions.py | 1 + .../revisions_tests/test_revisions.py | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/ravendb/documents/commands/revisions.py b/ravendb/documents/commands/revisions.py index 34c62b97..7b9e9ded 100644 --- a/ravendb/documents/commands/revisions.py +++ b/ravendb/documents/commands/revisions.py @@ -62,6 +62,7 @@ def from_change_vectors(cls, change_vectors: List[str] = None, metadata_only: bo def from_before(cls, id_: str, before: datetime) -> GetRevisionsCommand: if id_ is None: raise ValueError("Id cannot be None") + return GetRevisionsCommand(id_=id_, before=before) @classmethod def from_start_page( diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py index 9ab7e78d..e0df6ca1 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py @@ -1,3 +1,4 @@ +from datetime import datetime from time import sleep from ravendb import RevisionsConfiguration, RevisionsCollectionConfiguration @@ -350,3 +351,36 @@ def test_can_get_for_lazily(self): ids = [x.Id for x in revision] ids_lazily = [x.Id for x in revisions_lazily_result] self.assertEqual(ids, ids_lazily) + + def test_can_get_revisions_by_id_and_time_lazily(self): + id_ = "users/1" + id_2 = "users/2" + + self.setup_revisions(self.store, False, 100) + + with self.store.open_session() as session: + user1 = User() + user1.name = "Omer" + session.store(user1, id_) + + user2 = User() + user2.name = "Rhinos" + session.store(user2, id_2) + + session.save_changes() + + with self.store.open_session() as session: + revision = session.advanced.lazily.load(User, "users/1") + doc = revision.value + self.assertEqual(1, session.advanced.number_of_requests) + + with self.store.open_session() as session: + revision = session.advanced.revisions.get_by_before_date("users/1", datetime.utcnow(), User) + revisions_lazily = session.advanced.revisions.lazily.get_by_before_date("users/1", datetime.utcnow(), User) + session.advanced.revisions.lazily.get_by_before_date("users/2", datetime.utcnow(), User) + + revisions_lazily_result = revisions_lazily.value + + self.assertEqual(revisions_lazily_result.Id, revision.Id) + self.assertEqual(revisions_lazily_result.name, revision.name) + self.assertEqual(2, session.advanced.number_of_requests) From 7f214abc4e50712c6b8d4725576a633aa3c2df86 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Fri, 5 Jan 2024 15:39:45 +0100 Subject: [PATCH 29/33] RDBC-775 RevisionsTest::canGetNonExistingRevisionsByChangeVectorAsyncLazily --- ravendb/documents/operations/lazy/revisions.py | 4 +++- .../client_tests/revisions_tests/test_revisions.py | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/ravendb/documents/operations/lazy/revisions.py b/ravendb/documents/operations/lazy/revisions.py index 598ff8a3..a84b0ed7 100644 --- a/ravendb/documents/operations/lazy/revisions.py +++ b/ravendb/documents/operations/lazy/revisions.py @@ -97,8 +97,10 @@ def create_request(self) -> Optional[GetRequest]: def handle_response(self, response: GetResponse) -> None: if response.result is None: return - response_as_dict = json.loads(response.result) + + if response_as_dict is None: + return json_array = response_as_dict.get("Results") json_array_result = JsonArrayResult() diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py index e0df6ca1..5535b02e 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py @@ -384,3 +384,11 @@ def test_can_get_revisions_by_id_and_time_lazily(self): self.assertEqual(revisions_lazily_result.Id, revision.Id) self.assertEqual(revisions_lazily_result.name, revision.name) self.assertEqual(2, session.advanced.number_of_requests) + + def test_can_get_non_existing_revisions_by_change_vector_async_lazily(self): + with self.store.open_session() as session: + lazy = session.advanced.revisions.lazily.get_by_change_vector("dummy", User) + user = lazy.value + + self.assertEqual(1, session.advanced.number_of_requests) + self.assertIsNone(user) From e4fd246afb1307fbd81dc714767f35acb83a179c Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Fri, 5 Jan 2024 15:45:01 +0100 Subject: [PATCH 30/33] RDBC-775 RevisionsTest::canGetRevisionsByChangeVectorsLazily --- .../revisions_tests/test_revisions.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py index 5535b02e..63c0ab69 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py @@ -392,3 +392,36 @@ def test_can_get_non_existing_revisions_by_change_vector_async_lazily(self): self.assertEqual(1, session.advanced.number_of_requests) self.assertIsNone(user) + + def test_can_get_revisions_by_change_vectors_lazily(self): + id_ = "users/1" + + self.setup_revisions(self.store, False, 123) + + with self.store.open_session() as session: + user = User() + user.name = "Omer" + session.store(user, id_) + session.save_changes() + + for i in range(10): + with self.store.open_session() as session: + user = session.load(id_, Company) + user.name = f"Omer{i}" + session.save_changes() + + with self.store.open_session() as session: + revisions_metadata = session.advanced.revisions.get_metadata_for(id_) + self.assertEqual(11, len(revisions_metadata)) + + change_vectors = [x[constants.Documents.Metadata.CHANGE_VECTOR] for x in revisions_metadata] + change_vectors2 = [x[constants.Documents.Metadata.CHANGE_VECTOR] for x in revisions_metadata] + + revisions_lazy = session.advanced.revisions.lazily.get_by_change_vectors(change_vectors, User) + revisions_lazy2 = session.advanced.revisions.lazily.get_by_change_vectors(change_vectors2, User) + + lazy_result = revisions_lazy.value + revisions = session.advanced.revisions.get_by_change_vectors(change_vectors, User) + + self.assertEqual(3, session.advanced.number_of_requests) + self.assertEqual(revisions.keys(), lazy_result.keys()) From a83d3b7b206702192a97fe694d8cd4881b81baf8 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Fri, 5 Jan 2024 15:54:09 +0100 Subject: [PATCH 31/33] RDBC-775 RevisionsTest::canGetRevisionsByChangeVectorLazily --- .../documents/operations/lazy/revisions.py | 2 +- .../revisions_tests/test_revisions.py | 53 +++++++++++++++++-- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/ravendb/documents/operations/lazy/revisions.py b/ravendb/documents/operations/lazy/revisions.py index a84b0ed7..af6d19d3 100644 --- a/ravendb/documents/operations/lazy/revisions.py +++ b/ravendb/documents/operations/lazy/revisions.py @@ -22,7 +22,7 @@ class LazyRevisionOperations: def __init__(self, delegate: "DocumentSession"): self._delegate = delegate - def get_by_change_vector(self, change_vector: str, object_type: Type[_T]) -> "Lazy[List[_T]]": + def get_by_change_vector(self, change_vector: str, object_type: Type[_T]) -> "Lazy[_T]": operation = GetRevisionOperation.from_change_vector(self._delegate, change_vector) lazy_revision_operation = LazyRevisionOperation(operation, LazyRevisionOperation.Mode.SINGLE, object_type) return self._delegate.add_lazy_operation(object_type, lazy_revision_operation, None) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py index 63c0ab69..c1c73ff5 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py @@ -1,14 +1,12 @@ from datetime import datetime from time import sleep -from ravendb import RevisionsConfiguration, RevisionsCollectionConfiguration +from ravendb import RevisionsConfiguration, RevisionsCollectionConfiguration, GetStatisticsOperation from ravendb.documents.commands.revisions import GetRevisionsBinEntryCommand from ravendb.documents.operations.revisions import ConfigureRevisionsOperation, GetRevisionsOperation -from ravendb.documents.session.operations.operations import GetRevisionOperation from ravendb.infrastructure.entities import User from ravendb.infrastructure.orders import Company from ravendb.primitives import constants -from ravendb.primitives.constants import int_max from ravendb.tests.test_base import TestBase @@ -425,3 +423,52 @@ def test_can_get_revisions_by_change_vectors_lazily(self): self.assertEqual(3, session.advanced.number_of_requests) self.assertEqual(revisions.keys(), lazy_result.keys()) + + def test_can_get_revisions_by_change_vector_lazily(self): + id_ = "users/1" + id_2 = "users/2" + + self.setup_revisions(self.store, False, 123) + + with self.store.open_session() as session: + user1 = User() + user1.name = "Omer" + session.store(user1, id_) + + user2 = User() + user2.name = "Rhinos" + session.store(user2, id_2) + + session.save_changes() + + for i in range(10): + with self.store.open_session() as session: + user = session.load(id_, Company) + user.name = f"Omer{i}" + session.save_changes() + + stats = self.store.maintenance.send(GetStatisticsOperation()) + db_id = stats.database_id + + cv = f"A:23-{db_id}" + cv2 = f"A:3-{db_id}" + + with self.store.open_session() as session: + revisions = session.advanced.revisions.get_by_change_vector(cv, User) + revisions_lazily = session.advanced.revisions.lazily.get_by_change_vector(cv, User) + revisions_lazily1 = session.advanced.revisions.lazily.get_by_change_vector(cv2, User) + + revisions_lazily_value = revisions_lazily.value + + self.assertEqual(2, session.advanced.number_of_requests) + self.assertEqual(revisions.Id, revisions_lazily_value.Id) + self.assertEqual(revisions.name, revisions_lazily_value.name) + + with self.store.open_session() as session: + revisions = session.advanced.revisions.get_by_change_vector(cv, User) + revisions_lazily = session.advanced.revisions.lazily.get_by_change_vector(cv, User) + revisions_lazily_value = revisions_lazily.value + + self.assertEqual(2, session.advanced.number_of_requests) + self.assertEqual(revisions.Id, revisions_lazily_value.Id) + self.assertEqual(revisions.name, revisions_lazily_value.name) From a9e74cd7d047f1fd366ab742a35d89f52db309ac Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Fri, 5 Jan 2024 16:02:27 +0100 Subject: [PATCH 32/33] RDBC-775 fix flaky test - created a ticket https://issues.hibernatingrhinos.com/issue/RDBC-778 --- .../client_tests/revisions_tests/test_revisions.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py index c1c73ff5..b3a3f307 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py @@ -23,41 +23,28 @@ def test_revisions(self): session.store(user, "users/1") session.save_changes() - sleep(2) with self.store.open_session() as session: all_revisions = session.advanced.revisions.get_for("users/1", User) self.assertEqual(4, len(all_revisions)) self.assertEqual(["user4", "user3", "user2", "user1"], [x.name for x in all_revisions]) - sleep(2) revisions_skip_first = session.advanced.revisions.get_for("users/1", User, 1) self.assertEqual(3, len(revisions_skip_first)) self.assertEqual(["user3", "user2", "user1"], [x.name for x in revisions_skip_first]) - sleep(2) revisions_skip_first_take_two = session.advanced.revisions.get_for("users/1", User, 1, 2) self.assertEqual(2, len(revisions_skip_first_take_two)) self.assertEqual(["user3", "user2"], [x.name for x in revisions_skip_first_take_two]) - sleep(2) all_metadata = session.advanced.revisions.get_metadata_for("users/1") self.assertEqual(4, len(all_metadata)) - sleep(2) metadata_skip_first = session.advanced.revisions.get_metadata_for("users/1", 1) self.assertEqual(3, len(metadata_skip_first)) - sleep(2) metadata_skip_first_take_two = session.advanced.revisions.get_metadata_for("users/1", 1, 2) self.assertEqual(2, len(metadata_skip_first_take_two)) - sleep(2) - - user = session.advanced.revisions.get_by_change_vector( - metadata_skip_first[0].metadata.get(constants.Documents.Metadata.CHANGE_VECTOR), User - ) - self.assertEqual("user3", user.name) - def test_can_get_revisions_by_change_vector(self): id_ = "users/1" self.setup_revisions(self.store, False, 100) From 5962ab0b153ec5577da1ecf9293771f0cf5e3795 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Fri, 5 Jan 2024 16:26:04 +0100 Subject: [PATCH 33/33] RDBC-775 skip flaky test https://issues.hibernatingrhinos.com/issue/RDBC-779 --- .../client_tests/revisions_tests/test_revisions.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py index b3a3f307..6a0fd0ae 100644 --- a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions.py @@ -1,3 +1,4 @@ +import unittest from datetime import datetime from time import sleep @@ -411,6 +412,7 @@ def test_can_get_revisions_by_change_vectors_lazily(self): self.assertEqual(3, session.advanced.number_of_requests) self.assertEqual(revisions.keys(), lazy_result.keys()) + @unittest.skip("RDBC-779 Flaky test") def test_can_get_revisions_by_change_vector_lazily(self): id_ = "users/1" id_2 = "users/2" @@ -441,12 +443,15 @@ def test_can_get_revisions_by_change_vector_lazily(self): cv2 = f"A:3-{db_id}" with self.store.open_session() as session: + sleep(0.33) revisions = session.advanced.revisions.get_by_change_vector(cv, User) revisions_lazily = session.advanced.revisions.lazily.get_by_change_vector(cv, User) revisions_lazily1 = session.advanced.revisions.lazily.get_by_change_vector(cv2, User) + sleep(0.33) revisions_lazily_value = revisions_lazily.value + sleep(0.33) self.assertEqual(2, session.advanced.number_of_requests) self.assertEqual(revisions.Id, revisions_lazily_value.Id) self.assertEqual(revisions.name, revisions_lazily_value.name)