From 13b5b96215257bd2354514d2e51aa0c35438bf73 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Tue, 23 Jun 2026 14:24:25 +0200 Subject: [PATCH] RDBC-1079 Python API: Revisions Operations Add the missing revisions operations, ported from the C# client (v7.2): - EnforceRevisionsConfigurationOperation (IOperation) - AdoptOrphanedRevisionsOperation (IOperation) - DeleteRevisionsOperation (by id / date range / change vectors) - RevertRevisionsByIdOperation - ConfigureRevisionsBinCleanerOperation (+ RevisionsBinConfiguration) - ConfigureRevisionsForConflictsOperation (server-wide) - Supporting types: RevisionsOperationParameters, RevisionsOperationContinuationParameters, result classes Surface the full revisions operations set from the top-level ravendb package and add real-DB tests for each operation. --- ravendb/__init__.py | 21 +- ravendb/documents/operations/revisions.py | 363 +++++++++++++++++- ravendb/serverwide/operations/revisions.py | 65 ++++ .../test_revisions_operations.py | 184 +++++++++ ravendb/tests/test_imports.py | 20 +- 5 files changed, 643 insertions(+), 10 deletions(-) create mode 100644 ravendb/serverwide/operations/revisions.py create mode 100644 ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions_operations.py diff --git a/ravendb/__init__.py b/ravendb/__init__.py index ea8bde2c..038c0d7b 100644 --- a/ravendb/__init__.py +++ b/ravendb/__init__.py @@ -173,6 +173,23 @@ from ravendb.documents.operations.revisions import ( RevisionsCollectionConfiguration, RevisionsConfiguration, + RevisionsResult, + ConfigureRevisionsOperation, + ConfigureRevisionsOperationResult, + GetRevisionsOperation, + EnforceRevisionsConfigurationOperation, + AdoptOrphanedRevisionsOperation, + DeleteRevisionsOperation, + RevertRevisionsByIdOperation, + ConfigureRevisionsBinCleanerOperation, + ConfigureRevisionsBinCleanerOperationResult, + RevisionsBinConfiguration, + RevisionsOperationParameters, + RevisionsOperationContinuationParameters, +) +from ravendb.serverwide.operations.revisions import ( + ConfigureRevisionsForConflictsOperation, + ConfigureRevisionsForConflictsResult, ) from ravendb.documents.operations.statistics import ( GetCollectionStatisticsOperation, @@ -340,7 +357,6 @@ # todo: Serverwide # ReorderDatabaseMembersOperation -# ConfigureRevisionsForConflictsOperation # UpdateDatabaseOperation # GetServerWideBackupConfigurationOperation # SetDatabaseDynamicDistributionOperation @@ -420,9 +436,6 @@ # LazyRevisionOperation # LazyRevisionOperations # StreamOperation -# ConfigureRevisionsOperation -# GetRevisionsOperation -# RevisionsResult # GetConnectionStringsOperation # RemoveConnectionStringOperation # SqlEtlTable diff --git a/ravendb/documents/operations/revisions.py b/ravendb/documents/operations/revisions.py index a84731c1..2f7599ae 100644 --- a/ravendb/documents/operations/revisions.py +++ b/ravendb/documents/operations/revisions.py @@ -8,12 +8,18 @@ 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.documents.operations.definitions import ( + IOperation, + MaintenanceOperation, + OperationIdResult, + VoidOperation, +) +from ravendb.http.raven_command import RavenCommand, VoidRavenCommand from ravendb.util.util import RaftIdGenerator from ravendb.http.topology import RaftCommand from ravendb.documents.session.entity_to_json import EntityToJsonStatic from ravendb.documents.conventions import DocumentConventions +from ravendb.tools.utils import Utils if TYPE_CHECKING: from ravendb.http.http_cache import HttpCache @@ -216,3 +222,356 @@ def set_response(self, response: Optional[str], from_cache: bool) -> None: def get_raft_unique_request_id(self) -> str: return RaftIdGenerator().new_id() + + +class RevisionsOperationContinuationParameters: + """State for resuming an interrupted revisions operation (etags are node-local).""" + + def __init__( + self, + start_from_etags: Dict[str, int] = None, + etag_barriers: Dict[str, int] = None, + node_tags: Dict[str, str] = None, + ): + self.start_from_etags = start_from_etags + self.etag_barriers = etag_barriers + self.node_tags = node_tags + + def to_json(self) -> Dict[str, Any]: + return { + "StartFromEtags": self.start_from_etags, + "EtagBarriers": self.etag_barriers, + "NodeTags": self.node_tags, + } + + @classmethod + def from_json(cls, json_dict: Dict[str, Any]) -> RevisionsOperationContinuationParameters: + return cls( + json_dict.get("StartFromEtags"), + json_dict.get("EtagBarriers"), + json_dict.get("NodeTags"), + ) + + +class RevisionsOperationParameters: + """Base parameters shared by enforce-configuration and adopt-orphaned operations.""" + + def __init__( + self, + collections: List[str] = None, + continuation_parameters: RevisionsOperationContinuationParameters = None, + ): + self.collections = collections + self.continuation_parameters = continuation_parameters + + def to_json(self) -> Dict[str, Any]: + return { + "Collections": self.collections, + "ContinuationParameters": ( + self.continuation_parameters.to_json() if self.continuation_parameters else None + ), + } + + +class EnforceRevisionsConfigurationOperation(IOperation[OperationIdResult]): + """Applies the current revisions configuration to all existing revisions at once. + + This is a long-running operation - send it via ``store.operations.send_async`` and + await completion on the returned :class:`Operation`. + """ + + class Parameters(RevisionsOperationParameters): + def __init__( + self, + include_force_created: bool = False, + max_ops_per_second: int = None, + collections: List[str] = None, + continuation_parameters: RevisionsOperationContinuationParameters = None, + ): + super().__init__(collections, continuation_parameters) + if max_ops_per_second is not None and max_ops_per_second <= 0: + raise ValueError("max_ops_per_second must be greater than 0") + self.include_force_created = include_force_created + self.max_ops_per_second = max_ops_per_second + + def to_json(self) -> Dict[str, Any]: + json_dict = super().to_json() + json_dict["IncludeForceCreated"] = self.include_force_created + json_dict["MaxOpsPerSecond"] = self.max_ops_per_second + return json_dict + + def __init__(self, parameters: Optional["EnforceRevisionsConfigurationOperation.Parameters"] = None): + self._parameters = parameters if parameters is not None else EnforceRevisionsConfigurationOperation.Parameters() + + def get_command( + self, store: DocumentStore, conventions: DocumentConventions, cache: HttpCache + ) -> RavenCommand[OperationIdResult]: + return self.EnforceRevisionsConfigurationCommand(self._parameters) + + class EnforceRevisionsConfigurationCommand(RavenCommand[OperationIdResult]): + def __init__(self, parameters: "EnforceRevisionsConfigurationOperation.Parameters"): + super().__init__(OperationIdResult) + self._parameters = parameters + + 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/enforce" + request = requests.Request("POST", url) + request.data = self._parameters.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 = OperationIdResult.from_json(json.loads(response)) + + +class AdoptOrphanedRevisionsOperation(IOperation[OperationIdResult]): + """Re-attaches orphaned revisions (revisions of a deleted document missing from the + revisions bin) to their documents. + + Long-running - send via ``store.operations.send_async`` and await completion. + """ + + class Parameters(RevisionsOperationParameters): + pass + + def __init__(self, parameters: Optional["AdoptOrphanedRevisionsOperation.Parameters"] = None): + self._parameters = parameters if parameters is not None else AdoptOrphanedRevisionsOperation.Parameters() + + def get_command( + self, store: DocumentStore, conventions: DocumentConventions, cache: HttpCache + ) -> RavenCommand[OperationIdResult]: + return self.AdoptOrphanedRevisionsCommand(self._parameters) + + class AdoptOrphanedRevisionsCommand(RavenCommand[OperationIdResult]): + def __init__(self, parameters: "AdoptOrphanedRevisionsOperation.Parameters"): + super().__init__(OperationIdResult) + self._parameters = parameters + + 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/orphaned/adopt" + request = requests.Request("POST", url) + request.data = self._parameters.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 = OperationIdResult.from_json(json.loads(response)) + + +class DeleteRevisionsOperation(MaintenanceOperation["DeleteRevisionsOperation.Result"]): + """Explicitly deletes revisions for one or more documents - by document id(s), by a + date range, or by specific revision change-vectors.""" + + class Result: + def __init__(self, total_deletes: int = None): + self.total_deletes = total_deletes + + @classmethod + def from_json(cls, json_dict: Dict[str, Any]) -> DeleteRevisionsOperation.Result: + return cls(json_dict["TotalDeletes"]) + + class Parameters: + def __init__( + self, + document_ids: List[str] = None, + remove_force_created_revisions: bool = False, + revisions_change_vectors: List[str] = None, + from_date: datetime.datetime = None, + to_date: datetime.datetime = None, + ): + self.document_ids = document_ids + self.remove_force_created_revisions = remove_force_created_revisions + self.revisions_change_vectors = revisions_change_vectors + self.from_date = from_date + self.to_date = to_date + + def validate(self) -> None: + if not self.document_ids: + raise ValueError("Document ids cannot be None or empty") + + for document_id in self.document_ids: + if not document_id or document_id.isspace(): + raise ValueError("Document id cannot be None or whitespace") + + if self.revisions_change_vectors: + if len(self.document_ids) != 1: + raise ValueError("The number of document ids must be 1 when using revisions change vectors") + if self.from_date is not None or self.to_date is not None: + raise ValueError("Can't use revisions change vectors and date range in the same request") + elif self.from_date is not None and self.to_date is not None and self.to_date <= self.from_date: + raise ValueError("To date must be greater than From date") + + def to_json(self) -> Dict[str, Any]: + return { + "DocumentIds": self.document_ids, + "RevisionsChangeVectors": self.revisions_change_vectors, + "From": Utils.datetime_to_string(self.from_date) if self.from_date is not None else None, + "To": Utils.datetime_to_string(self.to_date) if self.to_date is not None else None, + "RemoveForceCreatedRevisions": self.remove_force_created_revisions, + } + + def __init__( + self, + document_id: str = None, + revisions_change_vectors: List[str] = None, + from_date: datetime.datetime = None, + to_date: datetime.datetime = None, + remove_force_created_revisions: bool = False, + document_ids: List[str] = None, + parameters: Optional["DeleteRevisionsOperation.Parameters"] = None, + ): + if parameters is None: + if document_ids is None and document_id is not None: + document_ids = [document_id] + parameters = DeleteRevisionsOperation.Parameters( + document_ids, + remove_force_created_revisions, + revisions_change_vectors, + from_date, + to_date, + ) + parameters.validate() + self._parameters = parameters + + def get_command(self, conventions: DocumentConventions) -> RavenCommand[DeleteRevisionsOperation.Result]: + return self.DeleteRevisionsCommand(self._parameters) + + class DeleteRevisionsCommand(RavenCommand["DeleteRevisionsOperation.Result"], RaftCommand): + def __init__(self, parameters: "DeleteRevisionsOperation.Parameters"): + super().__init__(DeleteRevisionsOperation.Result) + self._parameters = parameters + + 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" + request = requests.Request("DELETE", url) + request.data = self._parameters.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 = DeleteRevisionsOperation.Result.from_json(json.loads(response)) + + def get_raft_unique_request_id(self) -> str: + return RaftIdGenerator().new_id() + + +class RevertRevisionsByIdOperation(VoidOperation): + """Reverts one or more documents to a specific revision identified by its change-vector.""" + + def __init__(self, id_to_change_vector: Dict[str, str] = None, id_: str = None, change_vector: str = None): + if id_to_change_vector is None: + if not id_: + raise ValueError("Id cannot be None or empty") + if not change_vector: + raise ValueError("Change vector cannot be None or empty") + id_to_change_vector = {id_: change_vector} + + if not id_to_change_vector: + raise ValueError("id_to_change_vector cannot be None or empty") + + self._id_to_change_vector = id_to_change_vector + + def get_command(self, store: DocumentStore, conventions: DocumentConventions, cache: HttpCache) -> VoidRavenCommand: + return self.RevertRevisionsByIdCommand(self._id_to_change_vector) + + class RevertRevisionsByIdCommand(VoidRavenCommand): + def __init__(self, id_to_change_vector: Dict[str, str]): + super().__init__() + self._id_to_change_vector = id_to_change_vector + + def is_read_request(self) -> bool: + return False + + def create_request(self, node: ServerNode) -> requests.Request: + url = f"{node.url}/databases/{node.database}/revisions/revert/docs" + request = requests.Request("POST", url) + request.data = {"IdToChangeVector": self._id_to_change_vector} + return request + + +class RevisionsBinConfiguration: + """Configuration for the automatic revisions-bin cleaner.""" + + def __init__( + self, + disabled: bool = False, + minimum_entries_age_to_keep_in_min: int = 43200, + cleaner_frequency_in_sec: int = 300, + ): + self.disabled = disabled + self.minimum_entries_age_to_keep_in_min = minimum_entries_age_to_keep_in_min + self.cleaner_frequency_in_sec = cleaner_frequency_in_sec + + def to_json(self) -> Dict[str, Any]: + return { + "Disabled": self.disabled, + "MinimumEntriesAgeToKeepInMin": self.minimum_entries_age_to_keep_in_min, + "CleanerFrequencyInSec": self.cleaner_frequency_in_sec, + } + + @classmethod + def from_json(cls, json_dict: Dict[str, Any]) -> RevisionsBinConfiguration: + return cls( + json_dict.get("Disabled", False), + json_dict.get("MinimumEntriesAgeToKeepInMin"), + json_dict.get("CleanerFrequencyInSec"), + ) + + +class ConfigureRevisionsBinCleanerOperationResult: + 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]) -> ConfigureRevisionsBinCleanerOperationResult: + return cls(json_dict["RaftCommandIndex"]) + + +class ConfigureRevisionsBinCleanerOperation(MaintenanceOperation[ConfigureRevisionsBinCleanerOperationResult]): + """Enables / configures the automatic revisions-bin cleaner for the database.""" + + def __init__(self, configuration: RevisionsBinConfiguration): + if configuration is None: + raise ValueError("Configuration cannot be None") + self._configuration = configuration + + def get_command( + self, conventions: DocumentConventions + ) -> RavenCommand[ConfigureRevisionsBinCleanerOperationResult]: + return self.ConfigureRevisionsBinCleanerCommand(self._configuration) + + class ConfigureRevisionsBinCleanerCommand( + RavenCommand[ConfigureRevisionsBinCleanerOperationResult], RaftCommand + ): + def __init__(self, configuration: RevisionsBinConfiguration): + super().__init__(ConfigureRevisionsBinCleanerOperationResult) + 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/bin-cleaner/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 = ConfigureRevisionsBinCleanerOperationResult.from_json(json.loads(response)) + + def get_raft_unique_request_id(self) -> str: + return RaftIdGenerator().new_id() diff --git a/ravendb/serverwide/operations/revisions.py b/ravendb/serverwide/operations/revisions.py new file mode 100644 index 00000000..08d51598 --- /dev/null +++ b/ravendb/serverwide/operations/revisions.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import json +from typing import Any, Dict, Optional, TYPE_CHECKING + +import requests + +from ravendb.documents.operations.revisions import RevisionsCollectionConfiguration +from ravendb.http.raven_command import RavenCommand +from ravendb.http.topology import RaftCommand +from ravendb.serverwide.operations.common import ServerOperation +from ravendb.util.util import RaftIdGenerator + +if TYPE_CHECKING: + from ravendb.documents.conventions import DocumentConventions + from ravendb.http.server_node import ServerNode + + +class ConfigureRevisionsForConflictsResult: + def __init__(self, raft_command_index: Optional[int] = None): + self.raft_command_index = raft_command_index + + @classmethod + def from_json(cls, json_dict: Dict[str, Any]) -> ConfigureRevisionsForConflictsResult: + return cls(json_dict["RaftCommandIndex"]) + + +class ConfigureRevisionsForConflictsOperation(ServerOperation[ConfigureRevisionsForConflictsResult]): + """Sets the revisions configuration that is applied to conflicting documents of a database.""" + + def __init__(self, database: str, configuration: RevisionsCollectionConfiguration): + if configuration is None: + raise ValueError("Configuration cannot be None") + self._database = database + self._configuration = configuration + + def get_command(self, conventions: "DocumentConventions") -> RavenCommand[ConfigureRevisionsForConflictsResult]: + return self.ConfigureRevisionsForConflictsCommand(self._database, self._configuration) + + class ConfigureRevisionsForConflictsCommand( + RavenCommand[ConfigureRevisionsForConflictsResult], RaftCommand + ): + def __init__(self, database: str, configuration: RevisionsCollectionConfiguration): + super().__init__(ConfigureRevisionsForConflictsResult) + if database is None: + raise ValueError("Database cannot be None") + self._database = database + self._configuration = configuration + + def is_read_request(self) -> bool: + return False + + def create_request(self, node: "ServerNode") -> requests.Request: + url = f"{node.url}/databases/{self._database}/admin/revisions/conflicts/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 = ConfigureRevisionsForConflictsResult.from_json(json.loads(response)) + + def get_raft_unique_request_id(self) -> str: + return RaftIdGenerator().new_id() diff --git a/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions_operations.py b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions_operations.py new file mode 100644 index 00000000..d5cec7a7 --- /dev/null +++ b/ravendb/tests/jvm_migrated_tests/client_tests/revisions_tests/test_revisions_operations.py @@ -0,0 +1,184 @@ +from datetime import datetime + +from ravendb.documents.operations.revisions import ( + AdoptOrphanedRevisionsOperation, + ConfigureRevisionsBinCleanerOperation, + ConfigureRevisionsOperation, + DeleteRevisionsOperation, + EnforceRevisionsConfigurationOperation, + RevertRevisionsByIdOperation, + RevisionsBinConfiguration, + RevisionsCollectionConfiguration, + RevisionsConfiguration, +) +from ravendb.serverwide.operations.revisions import ConfigureRevisionsForConflictsOperation +from ravendb.infrastructure.orders import Company +from ravendb.primitives import constants +from ravendb.tests.test_base import TestBase + + +class TestRevisionsOperations(TestBase): + def setUp(self): + super().setUp() + + def _create_company_with_revisions(self, number_of_updates: int = 4) -> Company: + company = Company(name="Company Name") + with self.store.open_session() as session: + session.store(company) + session.save_changes() + + for i in range(number_of_updates): + with self.store.open_session() as session: + loaded = session.load(company.Id, Company) + loaded.name = f"Company {i}" + session.save_changes() + + return company + + def test_enforce_revisions_configuration_purges_excess_revisions(self): + self.setup_revisions(self.store, False, 100) + company = self._create_company_with_revisions(4) + + with self.store.open_session() as session: + self.assertEqual(5, session.advanced.revisions.get_count_for(company.Id)) + + # Tighten the configuration - this only affects future modifications... + configuration = RevisionsConfiguration() + default_config = RevisionsCollectionConfiguration() + default_config.minimum_revisions_to_keep = 2 + configuration.default_config = default_config + self.store.maintenance.send(ConfigureRevisionsOperation(configuration)) + + with self.store.open_session() as session: + self.assertEqual(5, session.advanced.revisions.get_count_for(company.Id)) + + # ...until we enforce it on the existing revisions. + operation = self.store.operations.send_async(EnforceRevisionsConfigurationOperation()) + operation.wait_for_completion() + + with self.store.open_session() as session: + self.assertEqual(2, session.advanced.revisions.get_count_for(company.Id)) + + def test_enforce_revisions_configuration_with_parameters(self): + self.setup_revisions(self.store, False, 100) + company = self._create_company_with_revisions(4) + + configuration = RevisionsConfiguration() + default_config = RevisionsCollectionConfiguration() + default_config.minimum_revisions_to_keep = 1 + configuration.default_config = default_config + self.store.maintenance.send(ConfigureRevisionsOperation(configuration)) + + parameters = EnforceRevisionsConfigurationOperation.Parameters( + include_force_created=True, collections=["Companies"] + ) + operation = self.store.operations.send_async(EnforceRevisionsConfigurationOperation(parameters)) + operation.wait_for_completion() + + with self.store.open_session() as session: + self.assertEqual(1, session.advanced.revisions.get_count_for(company.Id)) + + def test_delete_revisions_by_document_id(self): + self.setup_revisions(self.store, False, 100) + company = self._create_company_with_revisions(4) + + with self.store.open_session() as session: + self.assertEqual(5, session.advanced.revisions.get_count_for(company.Id)) + + result = self.store.maintenance.send(DeleteRevisionsOperation(document_id=company.Id)) + self.assertEqual(5, result.total_deletes) + + with self.store.open_session() as session: + self.assertEqual(0, session.advanced.revisions.get_count_for(company.Id)) + + def test_delete_revisions_by_change_vectors(self): + self.setup_revisions(self.store, False, 100) + company = self._create_company_with_revisions(4) + + with self.store.open_session() as session: + metadata = session.advanced.revisions.get_metadata_for(company.Id) + self.assertEqual(5, len(metadata)) + change_vectors = [m[constants.Documents.Metadata.CHANGE_VECTOR] for m in metadata[:2]] + + result = self.store.maintenance.send( + DeleteRevisionsOperation(document_id=company.Id, revisions_change_vectors=change_vectors) + ) + self.assertEqual(2, result.total_deletes) + + with self.store.open_session() as session: + self.assertEqual(3, session.advanced.revisions.get_count_for(company.Id)) + + def test_delete_revisions_validation_is_client_side(self): + self.assertRaises(ValueError, lambda: DeleteRevisionsOperation(document_ids=[])) + self.assertRaises( + ValueError, + lambda: DeleteRevisionsOperation( + document_id="companies/1", revisions_change_vectors=["cv"], from_date=datetime(2020, 1, 1) + ), + ) + + def test_revert_revisions_by_id(self): + self.setup_revisions(self.store, False, 100) + + company = Company(name="Old Name") + with self.store.open_session() as session: + session.store(company) + session.save_changes() + + with self.store.open_session() as session: + loaded = session.load(company.Id, Company) + loaded.name = "New Name" + session.save_changes() + + with self.store.open_session() as session: + metadata = session.advanced.revisions.get_metadata_for(company.Id) + self.assertEqual(2, len(metadata)) + # Metadata is ordered newest-first, so the original ("Old Name") revision is last. + old_change_vector = metadata[1][constants.Documents.Metadata.CHANGE_VECTOR] + + self.store.operations.send(RevertRevisionsByIdOperation(id_=company.Id, change_vector=old_change_vector)) + + with self.store.open_session() as session: + loaded = session.load(company.Id, Company) + self.assertEqual("Old Name", loaded.name) + + def test_configure_revisions_bin_cleaner(self): + configuration = RevisionsBinConfiguration() + configuration.disabled = False + configuration.minimum_entries_age_to_keep_in_min = 10 + configuration.cleaner_frequency_in_sec = 100 + + result = self.store.maintenance.send(ConfigureRevisionsBinCleanerOperation(configuration)) + + self.assertIsNotNone(result) + self.assertIsNotNone(result.raft_command_index) + self.assertGreater(result.raft_command_index, 0) + + def test_configure_revisions_for_conflicts(self): + configuration = RevisionsCollectionConfiguration() + configuration.minimum_revisions_to_keep = 5 + + result = self.store.maintenance.server.send( + ConfigureRevisionsForConflictsOperation(self.store.database, configuration) + ) + + self.assertIsNotNone(result) + self.assertIsNotNone(result.raft_command_index) + self.assertGreater(result.raft_command_index, 0) + + def test_adopt_orphaned_revisions_completes(self): + self.setup_revisions(self.store, False, 100) + + company = Company(name="Company Name") + with self.store.open_session() as session: + session.store(company) + session.save_changes() + + with self.store.open_session() as session: + session.delete(company.Id) + session.save_changes() + + # The operation must run to completion against the server (0 adoptions is a valid result); + # wait_for_completion raises if the operation faults. + operation = self.store.operations.send_async(AdoptOrphanedRevisionsOperation()) + operation.wait_for_completion() diff --git a/ravendb/tests/test_imports.py b/ravendb/tests/test_imports.py index 2edda75b..bdba36c2 100644 --- a/ravendb/tests/test_imports.py +++ b/ravendb/tests/test_imports.py @@ -29,7 +29,9 @@ def test_imports_at_top_level(self): from ravendb import GetBuildNumberOperation # from ravendb import ReorderDatabaseMembersOperation - # from ravendb import ConfigureRevisionsForConflictsOperation + from ravendb import ConfigureRevisionsForConflictsOperation + from ravendb import ConfigureRevisionsForConflictsResult + # from ravendb import UpdateDatabaseOperation # from ravendb import GetServerWideBackupConfigurationOperation # from ravendb import SetDatabaseDynamicDistributionOperation @@ -215,9 +217,19 @@ def test_imports_at_top_level(self): from ravendb import PatchResult from ravendb import PatchStatus - # from ravendb import ConfigureRevisionsOperation - # from ravendb import GetRevisionsOperation - # from ravendb import RevisionsResult + from ravendb import ConfigureRevisionsOperation + from ravendb import ConfigureRevisionsOperationResult + from ravendb import GetRevisionsOperation + from ravendb import RevisionsResult + from ravendb import EnforceRevisionsConfigurationOperation + from ravendb import AdoptOrphanedRevisionsOperation + from ravendb import DeleteRevisionsOperation + from ravendb import RevertRevisionsByIdOperation + from ravendb import ConfigureRevisionsBinCleanerOperation + from ravendb import ConfigureRevisionsBinCleanerOperationResult + from ravendb import RevisionsBinConfiguration + from ravendb import RevisionsOperationParameters + from ravendb import RevisionsOperationContinuationParameters from ravendb import RevisionsCollectionConfiguration from ravendb import RevisionsConfiguration from ravendb import DetailedDatabaseStatistics