diff --git a/ravendb/__init__.py b/ravendb/__init__.py index c920313b..93da1cd5 100644 --- a/ravendb/__init__.py +++ b/ravendb/__init__.py @@ -25,6 +25,7 @@ IndexSourceType, AutoIndexDefinition, AutoIndexFieldOptions, + ArchivedDataProcessingBehavior, ) from ravendb.documents.indexes.abstract_index_creation_tasks import ( AbstractIndexDefinitionBuilder, @@ -176,6 +177,9 @@ IndexInformation, GetDetailedStatisticsOperation, DetailedDatabaseStatistics, + GetEssentialStatisticsOperation, + EssentialDatabaseStatistics, + EssentialIndexInformation, ) from ravendb.documents.queries.explanation import ExplanationOptions, Explanations from ravendb.documents.queries.facets.builders import RangeBuilder, FacetBuilder, FacetOperations diff --git a/ravendb/documents/indexes/definitions.py b/ravendb/documents/indexes/definitions.py index 27d04376..b7b1b555 100644 --- a/ravendb/documents/indexes/definitions.py +++ b/ravendb/documents/indexes/definitions.py @@ -117,6 +117,15 @@ def __str__(self): return self.value +class ArchivedDataProcessingBehavior(Enum): + EXCLUDE_ARCHIVED = "ExcludeArchived" + INCLUDE_ARCHIVED = "IncludeArchived" + ARCHIVED_ONLY = "ArchivedOnly" + + def __str__(self): + return self.value + + class IndexType(Enum): NONE = "None" AUTO_MAP = "AutoMap" diff --git a/ravendb/documents/operations/statistics.py b/ravendb/documents/operations/statistics.py index e29c8bf5..eee5e1fb 100644 --- a/ravendb/documents/operations/statistics.py +++ b/ravendb/documents/operations/statistics.py @@ -6,7 +6,14 @@ import requests -from ravendb.documents.indexes.definitions import IndexPriority, IndexLockMode, IndexType, IndexSourceType, IndexState +from ravendb.documents.indexes.definitions import ( + ArchivedDataProcessingBehavior, + IndexPriority, + IndexLockMode, + IndexType, + IndexSourceType, + IndexState, +) from ravendb.documents.operations.definitions import MaintenanceOperation from ravendb.http.raven_command import RavenCommand from ravendb.http.server_node import ServerNode @@ -96,6 +103,109 @@ def from_json(cls, json_dict) -> DatabaseStatistics: ) +class EssentialIndexInformation: + def __init__( + self, + name: str = None, + lock_mode: IndexLockMode = None, + priority: IndexPriority = None, + index_type: IndexType = None, + source_type: IndexSourceType = None, + archived_data_processing_behavior: Optional[ArchivedDataProcessingBehavior] = None, + ): + self.name = name + self.lock_mode = lock_mode + self.priority = priority + self.type = index_type + self.source_type = source_type + self.archived_data_processing_behavior = archived_data_processing_behavior + + @classmethod + def from_json(cls, json_dict: dict) -> "EssentialIndexInformation": + return cls( + name=json_dict.get("Name"), + lock_mode=IndexLockMode(json_dict["LockMode"]) if json_dict.get("LockMode") else None, + priority=IndexPriority(json_dict["Priority"]) if json_dict.get("Priority") else None, + index_type=IndexType(json_dict["Type"]) if json_dict.get("Type") else None, + source_type=IndexSourceType(json_dict["SourceType"]) if json_dict.get("SourceType") else None, + archived_data_processing_behavior=( + ArchivedDataProcessingBehavior(json_dict["ArchivedDataProcessingBehavior"]) + if json_dict.get("ArchivedDataProcessingBehavior") + else None + ), + ) + + +class EssentialDatabaseStatistics: + def __init__( + self, + count_of_indexes: int = None, + count_of_documents: int = None, + count_of_revision_documents: int = None, + count_of_documents_conflicts: int = None, + count_of_tombstones: int = None, + count_of_conflicts: int = None, + count_of_attachments: int = None, + count_of_counter_entries: int = None, + count_of_time_series_segments: int = None, + indexes: List["EssentialIndexInformation"] = None, + ): + self.count_of_indexes = count_of_indexes + self.count_of_documents = count_of_documents + self.count_of_revision_documents = count_of_revision_documents + self.count_of_documents_conflicts = count_of_documents_conflicts + self.count_of_tombstones = count_of_tombstones + self.count_of_conflicts = count_of_conflicts + self.count_of_attachments = count_of_attachments + self.count_of_counter_entries = count_of_counter_entries + self.count_of_time_series_segments = count_of_time_series_segments + self.indexes = indexes + + @classmethod + def from_json(cls, json_dict: dict) -> "EssentialDatabaseStatistics": + return cls( + count_of_indexes=json_dict.get("CountOfIndexes"), + count_of_documents=json_dict.get("CountOfDocuments"), + count_of_revision_documents=json_dict.get("CountOfRevisionDocuments"), + count_of_documents_conflicts=json_dict.get("CountOfDocumentsConflicts"), + count_of_tombstones=json_dict.get("CountOfTombstones"), + count_of_conflicts=json_dict.get("CountOfConflicts"), + count_of_attachments=json_dict.get("CountOfAttachments"), + count_of_counter_entries=json_dict.get("CountOfCounterEntries"), + count_of_time_series_segments=json_dict.get("CountOfTimeSeriesSegments"), + indexes=( + [EssentialIndexInformation.from_json(x) for x in json_dict["Indexes"]] + if "Indexes" in json_dict + else None + ), + ) + + +class GetEssentialStatisticsOperation(MaintenanceOperation["EssentialDatabaseStatistics"]): + def __init__(self, debug_tag: str = None): + self._debug_tag = debug_tag + + def get_command(self, conventions: "DocumentConventions") -> RavenCommand["EssentialDatabaseStatistics"]: + return self._GetEssentialStatisticsCommand(self._debug_tag) + + class _GetEssentialStatisticsCommand(RavenCommand["EssentialDatabaseStatistics"]): + def __init__(self, debug_tag: Optional[str] = None): + super().__init__(EssentialDatabaseStatistics) + self._debug_tag = debug_tag + + def create_request(self, node: ServerNode) -> requests.Request: + url = f"{node.url}/databases/{node.database}/stats/essential" + if self._debug_tag is not None: + url += f"?{self._debug_tag}" + return requests.Request("GET", url) + + def set_response(self, response: str, from_cache: bool) -> None: + self.result = EssentialDatabaseStatistics.from_json(json.loads(response)) + + def is_read_request(self) -> bool: + return True + + class GetStatisticsOperation(MaintenanceOperation[DatabaseStatistics]): def __init__(self, debug_tag: str = None, node_tag: str = None): self.debug_tag = debug_tag diff --git a/ravendb/tests/issue_tests/test_RDBC_1033.py b/ravendb/tests/issue_tests/test_RDBC_1033.py new file mode 100644 index 00000000..7f7cd2fb --- /dev/null +++ b/ravendb/tests/issue_tests/test_RDBC_1033.py @@ -0,0 +1,174 @@ +""" +RDBC-1033: GetEssentialStatisticsOperation hits /stats/essential. + +C# reference: SlowTests.Issues/Issues/RavenDB_18648.cs + Can_Get_Essential_Database_Statistics() +""" + +import unittest + +from ravendb.documents.indexes.definitions import ( + ArchivedDataProcessingBehavior, + IndexLockMode, + IndexPriority, + IndexType, + IndexSourceType, +) +from ravendb.documents.operations.statistics import ( + EssentialDatabaseStatistics, + EssentialIndexInformation, + GetEssentialStatisticsOperation, +) +from ravendb.tests.test_base import TestBase + + +class TestEssentialStatisticsUnit(unittest.TestCase): + """Unit tests — no server required.""" + + def test_essential_database_statistics_from_json(self): + stats = EssentialDatabaseStatistics.from_json( + { + "CountOfIndexes": 5, + "CountOfDocuments": 42, + "CountOfRevisionDocuments": 10, + "CountOfDocumentsConflicts": 1, + "CountOfTombstones": 3, + "CountOfConflicts": 1, + "CountOfAttachments": 3, + "CountOfCounterEntries": 7, + "CountOfTimeSeriesSegments": 2, + } + ) + self.assertEqual(5, stats.count_of_indexes) + self.assertEqual(42, stats.count_of_documents) + self.assertEqual(10, stats.count_of_revision_documents) + self.assertEqual(1, stats.count_of_documents_conflicts) + self.assertEqual(3, stats.count_of_tombstones) + self.assertEqual(1, stats.count_of_conflicts) + self.assertEqual(3, stats.count_of_attachments) + self.assertEqual(7, stats.count_of_counter_entries) + self.assertEqual(2, stats.count_of_time_series_segments) + self.assertIsNone(stats.indexes) + + def test_essential_statistics_empty_json(self): + stats = EssentialDatabaseStatistics.from_json({}) + self.assertIsNone(stats.count_of_documents) + self.assertIsNone(stats.count_of_indexes) + self.assertIsNone(stats.count_of_revision_documents) + self.assertIsNone(stats.count_of_tombstones) + self.assertIsNone(stats.indexes) + + def test_essential_statistics_with_indexes(self): + stats = EssentialDatabaseStatistics.from_json( + { + "CountOfIndexes": 1, + "CountOfDocuments": 0, + "Indexes": [ + { + "Name": "Orders/ByCompany", + "LockMode": "Unlock", + "Priority": "Normal", + "Type": "Map", + "SourceType": "Documents", + } + ], + } + ) + self.assertIsNotNone(stats.indexes) + self.assertEqual(1, len(stats.indexes)) + idx = stats.indexes[0] + self.assertEqual("Orders/ByCompany", idx.name) + self.assertEqual(IndexLockMode.UNLOCK, idx.lock_mode) + self.assertEqual(IndexPriority.NORMAL, idx.priority) + self.assertEqual(IndexType.MAP, idx.type) + self.assertEqual(IndexSourceType.DOCUMENTS, idx.source_type) + self.assertIsNone(idx.archived_data_processing_behavior) + + def test_essential_index_information_archived_behavior(self): + idx = EssentialIndexInformation.from_json( + { + "Name": "idx", + "LockMode": "Unlock", + "Priority": "Normal", + "Type": "Map", + "SourceType": "Documents", + "ArchivedDataProcessingBehavior": "IncludeArchived", + } + ) + self.assertEqual(ArchivedDataProcessingBehavior.INCLUDE_ARCHIVED, idx.archived_data_processing_behavior) + + def test_essential_statistics_empty_indexes_list(self): + stats = EssentialDatabaseStatistics.from_json({"CountOfIndexes": 0, "Indexes": []}) + self.assertIsNotNone(stats.indexes) + self.assertEqual([], stats.indexes) + + def test_get_essential_statistics_operation_importable(self): + op = GetEssentialStatisticsOperation() + self.assertIsNotNone(op) + + def test_get_essential_statistics_url_contains_debug_tag(self): + from ravendb.http.server_node import ServerNode + from ravendb.documents.conventions import DocumentConventions + + node = ServerNode("http://localhost:8080", "testdb") + op = GetEssentialStatisticsOperation(debug_tag="src=test") + cmd = op.get_command(DocumentConventions()) + req = cmd.create_request(node) + self.assertIn("/stats/essential", req.url) + self.assertIn("src=test", req.url) + + def test_get_essential_statistics_url_no_debug_tag(self): + from ravendb.http.server_node import ServerNode + from ravendb.documents.conventions import DocumentConventions + + node = ServerNode("http://localhost:8080", "testdb") + op = GetEssentialStatisticsOperation() + cmd = op.get_command(DocumentConventions()) + req = cmd.create_request(node) + self.assertEqual("http://localhost:8080/databases/testdb/stats/essential", req.url) + + def test_top_level_package_exports(self): + import ravendb + + self.assertTrue(hasattr(ravendb, "GetEssentialStatisticsOperation")) + self.assertTrue(hasattr(ravendb, "EssentialDatabaseStatistics")) + self.assertTrue(hasattr(ravendb, "EssentialIndexInformation")) + self.assertTrue(hasattr(ravendb, "ArchivedDataProcessingBehavior")) + + +class TestEssentialStatistics(TestBase): + """Integration tests — require a live server.""" + + def setUp(self): + super().setUp() + self.store = self.get_document_store() + + def tearDown(self): + super().tearDown() + self.store.close() + + def test_get_essential_statistics_operation_returns_result(self): + from ravendb.infrastructure.orders import Product + + with self.store.open_session() as session: + p = Product() + p.name = "Widget" + session.store(p, "products/1") + session.save_changes() + + stats = self.store.maintenance.send(GetEssentialStatisticsOperation()) + + self.assertIsInstance(stats, EssentialDatabaseStatistics) + self.assertGreaterEqual(stats.count_of_documents, 1) + self.assertIsNotNone(stats.count_of_indexes) + + def test_get_essential_statistics_empty_database(self): + stats = self.store.maintenance.send(GetEssentialStatisticsOperation()) + self.assertIsInstance(stats, EssentialDatabaseStatistics) + self.assertEqual(0, stats.count_of_documents) + self.assertEqual(0, stats.count_of_tombstones) + self.assertEqual(0, stats.count_of_conflicts) + + +if __name__ == "__main__": + unittest.main()